github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/plan_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package command 5 6 import ( 7 "bytes" 8 "context" 9 "fmt" 10 "io/ioutil" 11 "os" 12 "path" 13 "path/filepath" 14 "strings" 15 "sync" 16 "testing" 17 "time" 18 19 "github.com/davecgh/go-spew/spew" 20 "github.com/zclconf/go-cty/cty" 21 22 "github.com/terramate-io/tf/addrs" 23 backendinit "github.com/terramate-io/tf/backend/init" 24 "github.com/terramate-io/tf/checks" 25 "github.com/terramate-io/tf/configs/configschema" 26 "github.com/terramate-io/tf/plans" 27 "github.com/terramate-io/tf/providers" 28 "github.com/terramate-io/tf/states" 29 "github.com/terramate-io/tf/terraform" 30 "github.com/terramate-io/tf/tfdiags" 31 ) 32 33 func TestPlan(t *testing.T) { 34 td := t.TempDir() 35 testCopyDir(t, testFixturePath("plan"), td) 36 defer testChdir(t, td)() 37 38 p := planFixtureProvider() 39 view, done := testView(t) 40 c := &PlanCommand{ 41 Meta: Meta{ 42 testingOverrides: metaOverridesForProvider(p), 43 View: view, 44 }, 45 } 46 47 args := []string{} 48 code := c.Run(args) 49 output := done(t) 50 if code != 0 { 51 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 52 } 53 } 54 55 func TestPlan_lockedState(t *testing.T) { 56 td := t.TempDir() 57 testCopyDir(t, testFixturePath("plan"), td) 58 defer testChdir(t, td)() 59 60 unlock, err := testLockState(t, testDataDir, filepath.Join(td, DefaultStateFilename)) 61 if err != nil { 62 t.Fatal(err) 63 } 64 defer unlock() 65 66 p := planFixtureProvider() 67 view, done := testView(t) 68 c := &PlanCommand{ 69 Meta: Meta{ 70 testingOverrides: metaOverridesForProvider(p), 71 View: view, 72 }, 73 } 74 75 args := []string{} 76 code := c.Run(args) 77 if code == 0 { 78 t.Fatal("expected error", done(t).Stdout()) 79 } 80 81 output := done(t).Stderr() 82 if !strings.Contains(output, "lock") { 83 t.Fatal("command output does not look like a lock error:", output) 84 } 85 } 86 87 func TestPlan_plan(t *testing.T) { 88 testCwd(t) 89 90 planPath := testPlanFileNoop(t) 91 92 p := testProvider() 93 view, done := testView(t) 94 c := &PlanCommand{ 95 Meta: Meta{ 96 testingOverrides: metaOverridesForProvider(p), 97 View: view, 98 }, 99 } 100 101 args := []string{planPath} 102 code := c.Run(args) 103 output := done(t) 104 if code != 1 { 105 t.Fatalf("wrong exit status %d; want 1\nstderr: %s", code, output.Stderr()) 106 } 107 } 108 109 func TestPlan_destroy(t *testing.T) { 110 td := t.TempDir() 111 testCopyDir(t, testFixturePath("plan"), td) 112 defer testChdir(t, td)() 113 114 originalState := states.BuildState(func(s *states.SyncState) { 115 s.SetResourceInstanceCurrent( 116 addrs.Resource{ 117 Mode: addrs.ManagedResourceMode, 118 Type: "test_instance", 119 Name: "foo", 120 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 121 &states.ResourceInstanceObjectSrc{ 122 AttrsJSON: []byte(`{"id":"bar"}`), 123 Status: states.ObjectReady, 124 }, 125 addrs.AbsProviderConfig{ 126 Provider: addrs.NewDefaultProvider("test"), 127 Module: addrs.RootModule, 128 }, 129 ) 130 }) 131 outPath := testTempFile(t) 132 statePath := testStateFile(t, originalState) 133 134 p := planFixtureProvider() 135 view, done := testView(t) 136 c := &PlanCommand{ 137 Meta: Meta{ 138 testingOverrides: metaOverridesForProvider(p), 139 View: view, 140 }, 141 } 142 143 args := []string{ 144 "-destroy", 145 "-out", outPath, 146 "-state", statePath, 147 } 148 code := c.Run(args) 149 output := done(t) 150 if code != 0 { 151 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 152 } 153 154 plan := testReadPlan(t, outPath) 155 for _, rc := range plan.Changes.Resources { 156 if got, want := rc.Action, plans.Delete; got != want { 157 t.Fatalf("wrong action %s for %s; want %s\nplanned change: %s", got, rc.Addr, want, spew.Sdump(rc)) 158 } 159 } 160 } 161 162 func TestPlan_noState(t *testing.T) { 163 td := t.TempDir() 164 testCopyDir(t, testFixturePath("plan"), td) 165 defer testChdir(t, td)() 166 167 p := planFixtureProvider() 168 view, done := testView(t) 169 c := &PlanCommand{ 170 Meta: Meta{ 171 testingOverrides: metaOverridesForProvider(p), 172 View: view, 173 }, 174 } 175 176 args := []string{} 177 code := c.Run(args) 178 output := done(t) 179 if code != 0 { 180 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 181 } 182 183 // Verify that refresh was called 184 if p.ReadResourceCalled { 185 t.Fatal("ReadResource should not be called") 186 } 187 188 // Verify that the provider was called with the existing state 189 actual := p.PlanResourceChangeRequest.PriorState 190 expected := cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["test_instance"].Block.ImpliedType()) 191 if !expected.RawEquals(actual) { 192 t.Fatalf("wrong prior state\ngot: %#v\nwant: %#v", actual, expected) 193 } 194 } 195 196 func TestPlan_generatedConfigPath(t *testing.T) { 197 td := t.TempDir() 198 testCopyDir(t, testFixturePath("plan-import-config-gen"), td) 199 defer testChdir(t, td)() 200 201 genPath := filepath.Join(td, "generated.tf") 202 203 p := planFixtureProvider() 204 view, done := testView(t) 205 206 c := &PlanCommand{ 207 Meta: Meta{ 208 testingOverrides: metaOverridesForProvider(p), 209 View: view, 210 }, 211 } 212 213 p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ 214 ImportedResources: []providers.ImportedResource{ 215 { 216 TypeName: "test_instance", 217 State: cty.ObjectVal(map[string]cty.Value{ 218 "id": cty.StringVal("bar"), 219 }), 220 Private: nil, 221 }, 222 }, 223 } 224 225 args := []string{ 226 "-generate-config-out", genPath, 227 } 228 code := c.Run(args) 229 output := done(t) 230 if code != 0 { 231 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 232 } 233 234 testFileEquals(t, genPath, filepath.Join(td, "generated.tf.expected")) 235 } 236 237 func TestPlan_outPath(t *testing.T) { 238 td := t.TempDir() 239 testCopyDir(t, testFixturePath("plan"), td) 240 defer testChdir(t, td)() 241 242 outPath := filepath.Join(td, "test.plan") 243 244 p := planFixtureProvider() 245 view, done := testView(t) 246 c := &PlanCommand{ 247 Meta: Meta{ 248 testingOverrides: metaOverridesForProvider(p), 249 View: view, 250 }, 251 } 252 253 p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{ 254 PlannedState: cty.NullVal(cty.EmptyObject), 255 } 256 257 args := []string{ 258 "-out", outPath, 259 } 260 code := c.Run(args) 261 output := done(t) 262 if code != 0 { 263 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 264 } 265 266 testReadPlan(t, outPath) // will call t.Fatal itself if the file cannot be read 267 } 268 269 func TestPlan_outPathNoChange(t *testing.T) { 270 td := t.TempDir() 271 testCopyDir(t, testFixturePath("plan"), td) 272 defer testChdir(t, td)() 273 274 originalState := states.BuildState(func(s *states.SyncState) { 275 s.SetResourceInstanceCurrent( 276 addrs.Resource{ 277 Mode: addrs.ManagedResourceMode, 278 Type: "test_instance", 279 Name: "foo", 280 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 281 &states.ResourceInstanceObjectSrc{ 282 // Aside from "id" (which is computed) the values here must 283 // exactly match the values in the "plan" test fixture in order 284 // to produce the empty plan we need for this test. 285 AttrsJSON: []byte(`{"id":"bar","ami":"bar","network_interface":[{"description":"Main network interface","device_index":"0"}]}`), 286 Status: states.ObjectReady, 287 }, 288 addrs.AbsProviderConfig{ 289 Provider: addrs.NewDefaultProvider("test"), 290 Module: addrs.RootModule, 291 }, 292 ) 293 }) 294 statePath := testStateFile(t, originalState) 295 296 outPath := filepath.Join(td, "test.plan") 297 298 p := planFixtureProvider() 299 view, done := testView(t) 300 c := &PlanCommand{ 301 Meta: Meta{ 302 testingOverrides: metaOverridesForProvider(p), 303 View: view, 304 }, 305 } 306 307 args := []string{ 308 "-out", outPath, 309 "-state", statePath, 310 } 311 code := c.Run(args) 312 output := done(t) 313 if code != 0 { 314 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 315 } 316 317 plan := testReadPlan(t, outPath) 318 if !plan.Changes.Empty() { 319 t.Fatalf("Expected empty plan to be written to plan file, got: %s", spew.Sdump(plan)) 320 } 321 } 322 323 func TestPlan_outPathWithError(t *testing.T) { 324 td := t.TempDir() 325 testCopyDir(t, testFixturePath("plan-fail-condition"), td) 326 defer testChdir(t, td)() 327 328 outPath := filepath.Join(td, "test.plan") 329 330 p := planFixtureProvider() 331 view, done := testView(t) 332 c := &PlanCommand{ 333 Meta: Meta{ 334 testingOverrides: metaOverridesForProvider(p), 335 View: view, 336 }, 337 } 338 339 p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{ 340 PlannedState: cty.NullVal(cty.EmptyObject), 341 } 342 343 args := []string{ 344 "-out", outPath, 345 } 346 code := c.Run(args) 347 output := done(t) 348 if code == 0 { 349 t.Fatal("expected non-zero exit status", output) 350 } 351 352 plan := testReadPlan(t, outPath) // will call t.Fatal itself if the file cannot be read 353 if !plan.Errored { 354 t.Fatal("plan should be marked with Errored") 355 } 356 357 if plan.Checks == nil { 358 t.Fatal("plan contains no checks") 359 } 360 361 // the checks should only contain one failure 362 results := plan.Checks.ConfigResults.Elements() 363 if len(results) != 1 { 364 t.Fatal("incorrect number of check results", len(results)) 365 } 366 if results[0].Value.Status != checks.StatusFail { 367 t.Errorf("incorrect status, got %s", results[0].Value.Status) 368 } 369 } 370 371 // When using "-out" with a backend, the plan should encode the backend config 372 func TestPlan_outBackend(t *testing.T) { 373 // Create a temporary working directory that is empty 374 td := t.TempDir() 375 testCopyDir(t, testFixturePath("plan-out-backend"), td) 376 defer testChdir(t, td)() 377 378 originalState := states.BuildState(func(s *states.SyncState) { 379 s.SetResourceInstanceCurrent( 380 addrs.Resource{ 381 Mode: addrs.ManagedResourceMode, 382 Type: "test_instance", 383 Name: "foo", 384 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 385 &states.ResourceInstanceObjectSrc{ 386 AttrsJSON: []byte(`{"id":"bar","ami":"bar"}`), 387 Status: states.ObjectReady, 388 }, 389 addrs.AbsProviderConfig{ 390 Provider: addrs.NewDefaultProvider("test"), 391 Module: addrs.RootModule, 392 }, 393 ) 394 }) 395 396 // Set up our backend state 397 dataState, srv := testBackendState(t, originalState, 200) 398 defer srv.Close() 399 testStateFileRemote(t, dataState) 400 401 outPath := "foo" 402 p := testProvider() 403 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 404 ResourceTypes: map[string]providers.Schema{ 405 "test_instance": { 406 Block: &configschema.Block{ 407 Attributes: map[string]*configschema.Attribute{ 408 "id": { 409 Type: cty.String, 410 Computed: true, 411 }, 412 "ami": { 413 Type: cty.String, 414 Optional: true, 415 }, 416 }, 417 }, 418 }, 419 }, 420 } 421 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 422 return providers.PlanResourceChangeResponse{ 423 PlannedState: req.ProposedNewState, 424 } 425 } 426 view, done := testView(t) 427 c := &PlanCommand{ 428 Meta: Meta{ 429 testingOverrides: metaOverridesForProvider(p), 430 View: view, 431 }, 432 } 433 434 args := []string{ 435 "-out", outPath, 436 } 437 code := c.Run(args) 438 output := done(t) 439 if code != 0 { 440 t.Logf("stdout: %s", output.Stdout()) 441 t.Fatalf("plan command failed with exit code %d\n\n%s", code, output.Stderr()) 442 } 443 444 plan := testReadPlan(t, outPath) 445 if !plan.Changes.Empty() { 446 t.Fatalf("Expected empty plan to be written to plan file, got: %s", spew.Sdump(plan)) 447 } 448 449 if got, want := plan.Backend.Type, "http"; got != want { 450 t.Errorf("wrong backend type %q; want %q", got, want) 451 } 452 if got, want := plan.Backend.Workspace, "default"; got != want { 453 t.Errorf("wrong backend workspace %q; want %q", got, want) 454 } 455 { 456 httpBackend := backendinit.Backend("http")() 457 schema := httpBackend.ConfigSchema() 458 got, err := plan.Backend.Config.Decode(schema.ImpliedType()) 459 if err != nil { 460 t.Fatalf("failed to decode backend config in plan: %s", err) 461 } 462 want, err := dataState.Backend.Config(schema) 463 if err != nil { 464 t.Fatalf("failed to decode cached config: %s", err) 465 } 466 if !want.RawEquals(got) { 467 t.Errorf("wrong backend config\ngot: %#v\nwant: %#v", got, want) 468 } 469 } 470 } 471 472 func TestPlan_refreshFalse(t *testing.T) { 473 // Create a temporary working directory that is empty 474 td := t.TempDir() 475 testCopyDir(t, testFixturePath("plan-existing-state"), td) 476 defer testChdir(t, td)() 477 478 p := planFixtureProvider() 479 view, done := testView(t) 480 c := &PlanCommand{ 481 Meta: Meta{ 482 testingOverrides: metaOverridesForProvider(p), 483 View: view, 484 }, 485 } 486 487 args := []string{ 488 "-refresh=false", 489 } 490 code := c.Run(args) 491 output := done(t) 492 if code != 0 { 493 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 494 } 495 496 if p.ReadResourceCalled { 497 t.Fatal("ReadResource should not have been called") 498 } 499 } 500 501 func TestPlan_refreshTrue(t *testing.T) { 502 // Create a temporary working directory that is empty 503 td := t.TempDir() 504 testCopyDir(t, testFixturePath("plan-existing-state"), td) 505 defer testChdir(t, td)() 506 507 p := planFixtureProvider() 508 view, done := testView(t) 509 c := &PlanCommand{ 510 Meta: Meta{ 511 testingOverrides: metaOverridesForProvider(p), 512 View: view, 513 }, 514 } 515 516 args := []string{ 517 "-refresh=true", 518 } 519 code := c.Run(args) 520 output := done(t) 521 if code != 0 { 522 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 523 } 524 525 if !p.ReadResourceCalled { 526 t.Fatalf("ReadResource should have been called") 527 } 528 } 529 530 // A consumer relies on the fact that running 531 // terraform plan -refresh=false -refresh=true gives the same result as 532 // terraform plan -refresh=true. 533 // While the flag logic itself is handled by the stdlib flags package (and code 534 // in main() that is tested elsewhere), we verify the overall plan command 535 // behaviour here in case we accidentally break this with additional logic. 536 func TestPlan_refreshFalseRefreshTrue(t *testing.T) { 537 // Create a temporary working directory that is empty 538 td := t.TempDir() 539 testCopyDir(t, testFixturePath("plan-existing-state"), td) 540 defer testChdir(t, td)() 541 542 p := planFixtureProvider() 543 view, done := testView(t) 544 c := &PlanCommand{ 545 Meta: Meta{ 546 testingOverrides: metaOverridesForProvider(p), 547 View: view, 548 }, 549 } 550 551 args := []string{ 552 "-refresh=false", 553 "-refresh=true", 554 } 555 code := c.Run(args) 556 output := done(t) 557 if code != 0 { 558 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 559 } 560 561 if !p.ReadResourceCalled { 562 t.Fatal("ReadResource should have been called") 563 } 564 } 565 566 func TestPlan_state(t *testing.T) { 567 // Create a temporary working directory that is empty 568 td := t.TempDir() 569 testCopyDir(t, testFixturePath("plan"), td) 570 defer testChdir(t, td)() 571 572 originalState := testState() 573 statePath := testStateFile(t, originalState) 574 575 p := planFixtureProvider() 576 view, done := testView(t) 577 c := &PlanCommand{ 578 Meta: Meta{ 579 testingOverrides: metaOverridesForProvider(p), 580 View: view, 581 }, 582 } 583 584 args := []string{ 585 "-state", statePath, 586 } 587 code := c.Run(args) 588 output := done(t) 589 if code != 0 { 590 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 591 } 592 593 // Verify that the provider was called with the existing state 594 actual := p.PlanResourceChangeRequest.PriorState 595 expected := cty.ObjectVal(map[string]cty.Value{ 596 "id": cty.StringVal("bar"), 597 "ami": cty.NullVal(cty.String), 598 "network_interface": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 599 "device_index": cty.String, 600 "description": cty.String, 601 })), 602 }) 603 if !expected.RawEquals(actual) { 604 t.Fatalf("wrong prior state\ngot: %#v\nwant: %#v", actual, expected) 605 } 606 } 607 608 func TestPlan_stateDefault(t *testing.T) { 609 // Create a temporary working directory that is empty 610 td := t.TempDir() 611 testCopyDir(t, testFixturePath("plan"), td) 612 defer testChdir(t, td)() 613 614 // Generate state and move it to the default path 615 originalState := testState() 616 statePath := testStateFile(t, originalState) 617 os.Rename(statePath, path.Join(td, "terraform.tfstate")) 618 619 p := planFixtureProvider() 620 view, done := testView(t) 621 c := &PlanCommand{ 622 Meta: Meta{ 623 testingOverrides: metaOverridesForProvider(p), 624 View: view, 625 }, 626 } 627 628 args := []string{} 629 code := c.Run(args) 630 output := done(t) 631 if code != 0 { 632 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 633 } 634 635 // Verify that the provider was called with the existing state 636 actual := p.PlanResourceChangeRequest.PriorState 637 expected := cty.ObjectVal(map[string]cty.Value{ 638 "id": cty.StringVal("bar"), 639 "ami": cty.NullVal(cty.String), 640 "network_interface": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 641 "device_index": cty.String, 642 "description": cty.String, 643 })), 644 }) 645 if !expected.RawEquals(actual) { 646 t.Fatalf("wrong prior state\ngot: %#v\nwant: %#v", actual, expected) 647 } 648 } 649 650 func TestPlan_validate(t *testing.T) { 651 // This is triggered by not asking for input so we have to set this to false 652 test = false 653 defer func() { test = true }() 654 655 td := t.TempDir() 656 testCopyDir(t, testFixturePath("plan-invalid"), td) 657 defer testChdir(t, td)() 658 659 p := testProvider() 660 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 661 ResourceTypes: map[string]providers.Schema{ 662 "test_instance": { 663 Block: &configschema.Block{ 664 Attributes: map[string]*configschema.Attribute{ 665 "id": {Type: cty.String, Optional: true, Computed: true}, 666 }, 667 }, 668 }, 669 }, 670 } 671 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 672 return providers.PlanResourceChangeResponse{ 673 PlannedState: req.ProposedNewState, 674 } 675 } 676 view, done := testView(t) 677 c := &PlanCommand{ 678 Meta: Meta{ 679 testingOverrides: metaOverridesForProvider(p), 680 View: view, 681 }, 682 } 683 684 args := []string{"-no-color"} 685 code := c.Run(args) 686 output := done(t) 687 if code != 1 { 688 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 689 } 690 691 actual := output.Stderr() 692 if want := "Error: Invalid count argument"; !strings.Contains(actual, want) { 693 t.Fatalf("unexpected error output\ngot:\n%s\n\nshould contain: %s", actual, want) 694 } 695 if want := "9: count = timestamp()"; !strings.Contains(actual, want) { 696 t.Fatalf("unexpected error output\ngot:\n%s\n\nshould contain: %s", actual, want) 697 } 698 } 699 700 func TestPlan_vars(t *testing.T) { 701 // Create a temporary working directory that is empty 702 td := t.TempDir() 703 testCopyDir(t, testFixturePath("plan-vars"), td) 704 defer testChdir(t, td)() 705 706 p := planVarsFixtureProvider() 707 view, done := testView(t) 708 c := &PlanCommand{ 709 Meta: Meta{ 710 testingOverrides: metaOverridesForProvider(p), 711 View: view, 712 }, 713 } 714 715 actual := "" 716 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 717 actual = req.ProposedNewState.GetAttr("value").AsString() 718 resp.PlannedState = req.ProposedNewState 719 return 720 } 721 722 args := []string{ 723 "-var", "foo=bar", 724 } 725 code := c.Run(args) 726 output := done(t) 727 if code != 0 { 728 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 729 } 730 731 if actual != "bar" { 732 t.Fatal("didn't work") 733 } 734 } 735 736 func TestPlan_varsInvalid(t *testing.T) { 737 testCases := []struct { 738 args []string 739 wantErr string 740 }{ 741 { 742 []string{"-var", "foo"}, 743 `The given -var option "foo" is not correctly specified.`, 744 }, 745 { 746 []string{"-var", "foo = bar"}, 747 `Variable name "foo " is invalid due to trailing space.`, 748 }, 749 } 750 751 // Create a temporary working directory that is empty 752 td := t.TempDir() 753 testCopyDir(t, testFixturePath("plan-vars"), td) 754 defer testChdir(t, td)() 755 756 for _, tc := range testCases { 757 t.Run(strings.Join(tc.args, " "), func(t *testing.T) { 758 p := planVarsFixtureProvider() 759 view, done := testView(t) 760 c := &PlanCommand{ 761 Meta: Meta{ 762 testingOverrides: metaOverridesForProvider(p), 763 View: view, 764 }, 765 } 766 767 code := c.Run(tc.args) 768 output := done(t) 769 if code != 1 { 770 t.Fatalf("bad: %d\n\n%s", code, output.Stdout()) 771 } 772 773 got := output.Stderr() 774 if !strings.Contains(got, tc.wantErr) { 775 t.Fatalf("bad error output, want %q, got:\n%s", tc.wantErr, got) 776 } 777 }) 778 } 779 } 780 781 func TestPlan_varsUnset(t *testing.T) { 782 // Create a temporary working directory that is empty 783 td := t.TempDir() 784 testCopyDir(t, testFixturePath("plan-vars"), td) 785 defer testChdir(t, td)() 786 787 // The plan command will prompt for interactive input of var.foo. 788 // We'll answer "bar" to that prompt, which should then allow this 789 // configuration to apply even though var.foo doesn't have a 790 // default value and there are no -var arguments on our command line. 791 792 // This will (helpfully) panic if more than one variable is requested during plan: 793 // https://github.com/terramate-io/tf/issues/26027 794 close := testInteractiveInput(t, []string{"bar"}) 795 defer close() 796 797 p := planVarsFixtureProvider() 798 view, done := testView(t) 799 c := &PlanCommand{ 800 Meta: Meta{ 801 testingOverrides: metaOverridesForProvider(p), 802 View: view, 803 }, 804 } 805 806 args := []string{} 807 code := c.Run(args) 808 output := done(t) 809 if code != 0 { 810 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 811 } 812 } 813 814 // This test adds a required argument to the test provider to validate 815 // processing of user input: 816 // https://github.com/terramate-io/tf/issues/26035 817 func TestPlan_providerArgumentUnset(t *testing.T) { 818 // Create a temporary working directory that is empty 819 td := t.TempDir() 820 testCopyDir(t, testFixturePath("plan"), td) 821 defer testChdir(t, td)() 822 823 // Disable test mode so input would be asked 824 test = false 825 defer func() { test = true }() 826 827 // The plan command will prompt for interactive input of provider.test.region 828 defaultInputReader = bytes.NewBufferString("us-east-1\n") 829 830 p := planFixtureProvider() 831 // override the planFixtureProvider schema to include a required provider argument 832 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 833 Provider: providers.Schema{ 834 Block: &configschema.Block{ 835 Attributes: map[string]*configschema.Attribute{ 836 "region": {Type: cty.String, Required: true}, 837 }, 838 }, 839 }, 840 ResourceTypes: map[string]providers.Schema{ 841 "test_instance": { 842 Block: &configschema.Block{ 843 Attributes: map[string]*configschema.Attribute{ 844 "id": {Type: cty.String, Optional: true, Computed: true}, 845 "ami": {Type: cty.String, Optional: true, Computed: true}, 846 }, 847 BlockTypes: map[string]*configschema.NestedBlock{ 848 "network_interface": { 849 Nesting: configschema.NestingList, 850 Block: configschema.Block{ 851 Attributes: map[string]*configschema.Attribute{ 852 "device_index": {Type: cty.String, Optional: true}, 853 "description": {Type: cty.String, Optional: true}, 854 }, 855 }, 856 }, 857 }, 858 }, 859 }, 860 }, 861 DataSources: map[string]providers.Schema{ 862 "test_data_source": { 863 Block: &configschema.Block{ 864 Attributes: map[string]*configschema.Attribute{ 865 "id": { 866 Type: cty.String, 867 Required: true, 868 }, 869 "valid": { 870 Type: cty.Bool, 871 Computed: true, 872 }, 873 }, 874 }, 875 }, 876 }, 877 } 878 view, done := testView(t) 879 c := &PlanCommand{ 880 Meta: Meta{ 881 testingOverrides: metaOverridesForProvider(p), 882 View: view, 883 }, 884 } 885 886 args := []string{} 887 code := c.Run(args) 888 output := done(t) 889 if code != 0 { 890 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 891 } 892 } 893 894 // Test that terraform properly merges provider configuration that's split 895 // between config files and interactive input variables. 896 // https://github.com/terramate-io/tf/issues/28956 897 func TestPlan_providerConfigMerge(t *testing.T) { 898 td := t.TempDir() 899 testCopyDir(t, testFixturePath("plan-provider-input"), td) 900 defer testChdir(t, td)() 901 902 // Disable test mode so input would be asked 903 test = false 904 defer func() { test = true }() 905 906 // The plan command will prompt for interactive input of provider.test.region 907 defaultInputReader = bytes.NewBufferString("us-east-1\n") 908 909 p := planFixtureProvider() 910 // override the planFixtureProvider schema to include a required provider argument and a nested block 911 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 912 Provider: providers.Schema{ 913 Block: &configschema.Block{ 914 Attributes: map[string]*configschema.Attribute{ 915 "region": {Type: cty.String, Required: true}, 916 "url": {Type: cty.String, Required: true}, 917 }, 918 BlockTypes: map[string]*configschema.NestedBlock{ 919 "auth": { 920 Nesting: configschema.NestingList, 921 Block: configschema.Block{ 922 Attributes: map[string]*configschema.Attribute{ 923 "user": {Type: cty.String, Required: true}, 924 "password": {Type: cty.String, Required: true}, 925 }, 926 }, 927 }, 928 }, 929 }, 930 }, 931 ResourceTypes: map[string]providers.Schema{ 932 "test_instance": { 933 Block: &configschema.Block{ 934 Attributes: map[string]*configschema.Attribute{ 935 "id": {Type: cty.String, Optional: true, Computed: true}, 936 }, 937 }, 938 }, 939 }, 940 } 941 942 view, done := testView(t) 943 c := &PlanCommand{ 944 Meta: Meta{ 945 testingOverrides: metaOverridesForProvider(p), 946 View: view, 947 }, 948 } 949 950 args := []string{} 951 code := c.Run(args) 952 output := done(t) 953 if code != 0 { 954 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 955 } 956 957 if !p.ConfigureProviderCalled { 958 t.Fatal("configure provider not called") 959 } 960 961 // For this test, we want to confirm that we've sent the expected config 962 // value *to* the provider. 963 got := p.ConfigureProviderRequest.Config 964 want := cty.ObjectVal(map[string]cty.Value{ 965 "auth": cty.ListVal([]cty.Value{ 966 cty.ObjectVal(map[string]cty.Value{ 967 "user": cty.StringVal("one"), 968 "password": cty.StringVal("onepw"), 969 }), 970 cty.ObjectVal(map[string]cty.Value{ 971 "user": cty.StringVal("two"), 972 "password": cty.StringVal("twopw"), 973 }), 974 }), 975 "region": cty.StringVal("us-east-1"), 976 "url": cty.StringVal("example.com"), 977 }) 978 979 if !got.RawEquals(want) { 980 t.Fatal("wrong provider config") 981 } 982 983 } 984 985 func TestPlan_varFile(t *testing.T) { 986 // Create a temporary working directory that is empty 987 td := t.TempDir() 988 testCopyDir(t, testFixturePath("plan-vars"), td) 989 defer testChdir(t, td)() 990 991 varFilePath := testTempFile(t) 992 if err := ioutil.WriteFile(varFilePath, []byte(planVarFile), 0644); err != nil { 993 t.Fatalf("err: %s", err) 994 } 995 996 p := planVarsFixtureProvider() 997 view, done := testView(t) 998 c := &PlanCommand{ 999 Meta: Meta{ 1000 testingOverrides: metaOverridesForProvider(p), 1001 View: view, 1002 }, 1003 } 1004 1005 actual := "" 1006 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 1007 actual = req.ProposedNewState.GetAttr("value").AsString() 1008 resp.PlannedState = req.ProposedNewState 1009 return 1010 } 1011 1012 args := []string{ 1013 "-var-file", varFilePath, 1014 } 1015 code := c.Run(args) 1016 output := done(t) 1017 if code != 0 { 1018 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 1019 } 1020 1021 if actual != "bar" { 1022 t.Fatal("didn't work") 1023 } 1024 } 1025 1026 func TestPlan_varFileDefault(t *testing.T) { 1027 // Create a temporary working directory that is empty 1028 td := t.TempDir() 1029 testCopyDir(t, testFixturePath("plan-vars"), td) 1030 defer testChdir(t, td)() 1031 1032 varFilePath := filepath.Join(td, "terraform.tfvars") 1033 if err := ioutil.WriteFile(varFilePath, []byte(planVarFile), 0644); err != nil { 1034 t.Fatalf("err: %s", err) 1035 } 1036 1037 p := planVarsFixtureProvider() 1038 view, done := testView(t) 1039 c := &PlanCommand{ 1040 Meta: Meta{ 1041 testingOverrides: metaOverridesForProvider(p), 1042 View: view, 1043 }, 1044 } 1045 1046 actual := "" 1047 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 1048 actual = req.ProposedNewState.GetAttr("value").AsString() 1049 resp.PlannedState = req.ProposedNewState 1050 return 1051 } 1052 1053 args := []string{} 1054 code := c.Run(args) 1055 output := done(t) 1056 if code != 0 { 1057 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 1058 } 1059 1060 if actual != "bar" { 1061 t.Fatal("didn't work") 1062 } 1063 } 1064 1065 func TestPlan_varFileWithDecls(t *testing.T) { 1066 // Create a temporary working directory that is empty 1067 td := t.TempDir() 1068 testCopyDir(t, testFixturePath("plan-vars"), td) 1069 defer testChdir(t, td)() 1070 1071 varFilePath := testTempFile(t) 1072 if err := ioutil.WriteFile(varFilePath, []byte(planVarFileWithDecl), 0644); err != nil { 1073 t.Fatalf("err: %s", err) 1074 } 1075 1076 p := planVarsFixtureProvider() 1077 view, done := testView(t) 1078 c := &PlanCommand{ 1079 Meta: Meta{ 1080 testingOverrides: metaOverridesForProvider(p), 1081 View: view, 1082 }, 1083 } 1084 1085 args := []string{ 1086 "-var-file", varFilePath, 1087 } 1088 code := c.Run(args) 1089 output := done(t) 1090 if code == 0 { 1091 t.Fatalf("succeeded; want failure\n\n%s", output.Stdout()) 1092 } 1093 1094 msg := output.Stderr() 1095 if got, want := msg, "Variable declaration in .tfvars file"; !strings.Contains(got, want) { 1096 t.Fatalf("missing expected error message\nwant message containing %q\ngot:\n%s", want, got) 1097 } 1098 } 1099 1100 func TestPlan_detailedExitcode(t *testing.T) { 1101 td := t.TempDir() 1102 testCopyDir(t, testFixturePath("plan"), td) 1103 defer testChdir(t, td)() 1104 1105 t.Run("return 1", func(t *testing.T) { 1106 view, done := testView(t) 1107 c := &PlanCommand{ 1108 Meta: Meta{ 1109 // Running plan without setting testingOverrides is similar to plan without init 1110 View: view, 1111 }, 1112 } 1113 code := c.Run([]string{"-detailed-exitcode"}) 1114 output := done(t) 1115 if code != 1 { 1116 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 1117 } 1118 }) 1119 1120 t.Run("return 2", func(t *testing.T) { 1121 p := planFixtureProvider() 1122 view, done := testView(t) 1123 c := &PlanCommand{ 1124 Meta: Meta{ 1125 testingOverrides: metaOverridesForProvider(p), 1126 View: view, 1127 }, 1128 } 1129 1130 code := c.Run([]string{"-detailed-exitcode"}) 1131 output := done(t) 1132 if code != 2 { 1133 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 1134 } 1135 }) 1136 } 1137 1138 func TestPlan_detailedExitcode_emptyDiff(t *testing.T) { 1139 td := t.TempDir() 1140 testCopyDir(t, testFixturePath("plan-emptydiff"), td) 1141 defer testChdir(t, td)() 1142 1143 p := testProvider() 1144 view, done := testView(t) 1145 c := &PlanCommand{ 1146 Meta: Meta{ 1147 testingOverrides: metaOverridesForProvider(p), 1148 View: view, 1149 }, 1150 } 1151 1152 args := []string{"-detailed-exitcode"} 1153 code := c.Run(args) 1154 output := done(t) 1155 if code != 0 { 1156 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 1157 } 1158 } 1159 1160 func TestPlan_shutdown(t *testing.T) { 1161 // Create a temporary working directory that is empty 1162 td := t.TempDir() 1163 testCopyDir(t, testFixturePath("apply-shutdown"), td) 1164 defer testChdir(t, td)() 1165 1166 cancelled := make(chan struct{}) 1167 shutdownCh := make(chan struct{}) 1168 1169 p := testProvider() 1170 view, done := testView(t) 1171 c := &PlanCommand{ 1172 Meta: Meta{ 1173 testingOverrides: metaOverridesForProvider(p), 1174 View: view, 1175 ShutdownCh: shutdownCh, 1176 }, 1177 } 1178 1179 p.StopFn = func() error { 1180 close(cancelled) 1181 return nil 1182 } 1183 1184 var once sync.Once 1185 1186 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 1187 once.Do(func() { 1188 shutdownCh <- struct{}{} 1189 }) 1190 1191 // Because of the internal lock in the MockProvider, we can't 1192 // coordinate directly with the calling of Stop, and making the 1193 // MockProvider concurrent is disruptive to a lot of existing tests. 1194 // Wait here a moment to help make sure the main goroutine gets to the 1195 // Stop call before we exit, or the plan may finish before it can be 1196 // canceled. 1197 time.Sleep(200 * time.Millisecond) 1198 1199 s := req.ProposedNewState.AsValueMap() 1200 s["ami"] = cty.StringVal("bar") 1201 resp.PlannedState = cty.ObjectVal(s) 1202 return 1203 } 1204 1205 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 1206 ResourceTypes: map[string]providers.Schema{ 1207 "test_instance": { 1208 Block: &configschema.Block{ 1209 Attributes: map[string]*configschema.Attribute{ 1210 "ami": {Type: cty.String, Optional: true}, 1211 }, 1212 }, 1213 }, 1214 }, 1215 } 1216 1217 code := c.Run([]string{}) 1218 output := done(t) 1219 if code != 1 { 1220 t.Errorf("wrong exit code %d; want 1\noutput:\n%s", code, output.Stdout()) 1221 } 1222 1223 select { 1224 case <-cancelled: 1225 default: 1226 t.Error("command not cancelled") 1227 } 1228 } 1229 1230 func TestPlan_init_required(t *testing.T) { 1231 td := t.TempDir() 1232 testCopyDir(t, testFixturePath("plan"), td) 1233 defer testChdir(t, td)() 1234 1235 view, done := testView(t) 1236 c := &PlanCommand{ 1237 Meta: Meta{ 1238 // Running plan without setting testingOverrides is similar to plan without init 1239 View: view, 1240 }, 1241 } 1242 1243 args := []string{"-no-color"} 1244 code := c.Run(args) 1245 output := done(t) 1246 if code != 1 { 1247 t.Fatalf("expected error, got success") 1248 } 1249 got := output.Stderr() 1250 if !(strings.Contains(got, "terraform init") && strings.Contains(got, "provider registry.terraform.io/hashicorp/test: required by this configuration but no version is selected")) { 1251 t.Fatal("wrong error message in output:", got) 1252 } 1253 } 1254 1255 // Config with multiple resources, targeting plan of a subset 1256 func TestPlan_targeted(t *testing.T) { 1257 td := t.TempDir() 1258 testCopyDir(t, testFixturePath("apply-targeted"), td) 1259 defer testChdir(t, td)() 1260 1261 p := testProvider() 1262 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 1263 ResourceTypes: map[string]providers.Schema{ 1264 "test_instance": { 1265 Block: &configschema.Block{ 1266 Attributes: map[string]*configschema.Attribute{ 1267 "id": {Type: cty.String, Computed: true}, 1268 }, 1269 }, 1270 }, 1271 }, 1272 } 1273 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 1274 return providers.PlanResourceChangeResponse{ 1275 PlannedState: req.ProposedNewState, 1276 } 1277 } 1278 1279 view, done := testView(t) 1280 c := &PlanCommand{ 1281 Meta: Meta{ 1282 testingOverrides: metaOverridesForProvider(p), 1283 View: view, 1284 }, 1285 } 1286 1287 args := []string{ 1288 "-target", "test_instance.foo", 1289 "-target", "test_instance.baz", 1290 } 1291 code := c.Run(args) 1292 output := done(t) 1293 if code != 0 { 1294 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 1295 } 1296 1297 if got, want := output.Stdout(), "3 to add, 0 to change, 0 to destroy"; !strings.Contains(got, want) { 1298 t.Fatalf("bad change summary, want %q, got:\n%s", want, got) 1299 } 1300 } 1301 1302 // Diagnostics for invalid -target flags 1303 func TestPlan_targetFlagsDiags(t *testing.T) { 1304 testCases := map[string]string{ 1305 "test_instance.": "Dot must be followed by attribute name.", 1306 "test_instance": "Resource specification must include a resource type and name.", 1307 } 1308 1309 for target, wantDiag := range testCases { 1310 t.Run(target, func(t *testing.T) { 1311 td := testTempDir(t) 1312 defer os.RemoveAll(td) 1313 defer testChdir(t, td)() 1314 1315 view, done := testView(t) 1316 c := &PlanCommand{ 1317 Meta: Meta{ 1318 View: view, 1319 }, 1320 } 1321 1322 args := []string{ 1323 "-target", target, 1324 } 1325 code := c.Run(args) 1326 output := done(t) 1327 if code != 1 { 1328 t.Fatalf("bad: %d\n\n%s", code, output.Stdout()) 1329 } 1330 1331 got := output.Stderr() 1332 if !strings.Contains(got, target) { 1333 t.Fatalf("bad error output, want %q, got:\n%s", target, got) 1334 } 1335 if !strings.Contains(got, wantDiag) { 1336 t.Fatalf("bad error output, want %q, got:\n%s", wantDiag, got) 1337 } 1338 }) 1339 } 1340 } 1341 1342 func TestPlan_replace(t *testing.T) { 1343 td := t.TempDir() 1344 testCopyDir(t, testFixturePath("plan-replace"), td) 1345 defer testChdir(t, td)() 1346 1347 originalState := states.BuildState(func(s *states.SyncState) { 1348 s.SetResourceInstanceCurrent( 1349 addrs.Resource{ 1350 Mode: addrs.ManagedResourceMode, 1351 Type: "test_instance", 1352 Name: "a", 1353 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 1354 &states.ResourceInstanceObjectSrc{ 1355 AttrsJSON: []byte(`{"id":"hello"}`), 1356 Status: states.ObjectReady, 1357 }, 1358 addrs.AbsProviderConfig{ 1359 Provider: addrs.NewDefaultProvider("test"), 1360 Module: addrs.RootModule, 1361 }, 1362 ) 1363 }) 1364 statePath := testStateFile(t, originalState) 1365 1366 p := testProvider() 1367 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 1368 ResourceTypes: map[string]providers.Schema{ 1369 "test_instance": { 1370 Block: &configschema.Block{ 1371 Attributes: map[string]*configschema.Attribute{ 1372 "id": {Type: cty.String, Computed: true}, 1373 }, 1374 }, 1375 }, 1376 }, 1377 } 1378 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 1379 return providers.PlanResourceChangeResponse{ 1380 PlannedState: req.ProposedNewState, 1381 } 1382 } 1383 1384 view, done := testView(t) 1385 c := &PlanCommand{ 1386 Meta: Meta{ 1387 testingOverrides: metaOverridesForProvider(p), 1388 View: view, 1389 }, 1390 } 1391 1392 args := []string{ 1393 "-state", statePath, 1394 "-no-color", 1395 "-replace", "test_instance.a", 1396 } 1397 code := c.Run(args) 1398 output := done(t) 1399 if code != 0 { 1400 t.Fatalf("wrong exit code %d\n\n%s", code, output.Stderr()) 1401 } 1402 1403 stdout := output.Stdout() 1404 if got, want := stdout, "1 to add, 0 to change, 1 to destroy"; !strings.Contains(got, want) { 1405 t.Errorf("wrong plan summary\ngot output:\n%s\n\nwant substring: %s", got, want) 1406 } 1407 if got, want := stdout, "test_instance.a will be replaced, as requested"; !strings.Contains(got, want) { 1408 t.Errorf("missing replace explanation\ngot output:\n%s\n\nwant substring: %s", got, want) 1409 } 1410 } 1411 1412 // Verify that the parallelism flag allows no more than the desired number of 1413 // concurrent calls to PlanResourceChange. 1414 func TestPlan_parallelism(t *testing.T) { 1415 // Create a temporary working directory that is empty 1416 td := t.TempDir() 1417 testCopyDir(t, testFixturePath("parallelism"), td) 1418 defer testChdir(t, td)() 1419 1420 par := 4 1421 1422 // started is a semaphore that we use to ensure that we never have more 1423 // than "par" plan operations happening concurrently 1424 started := make(chan struct{}, par) 1425 1426 // beginCtx is used as a starting gate to hold back PlanResourceChange 1427 // calls until we reach the desired concurrency. The cancel func "begin" is 1428 // called once we reach the desired concurrency, allowing all apply calls 1429 // to proceed in unison. 1430 beginCtx, begin := context.WithCancel(context.Background()) 1431 1432 // Since our mock provider has its own mutex preventing concurrent calls 1433 // to ApplyResourceChange, we need to use a number of separate providers 1434 // here. They will all have the same mock implementation function assigned 1435 // but crucially they will each have their own mutex. 1436 providerFactories := map[addrs.Provider]providers.Factory{} 1437 for i := 0; i < 10; i++ { 1438 name := fmt.Sprintf("test%d", i) 1439 provider := &terraform.MockProvider{} 1440 provider.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 1441 ResourceTypes: map[string]providers.Schema{ 1442 name + "_instance": {Block: &configschema.Block{}}, 1443 }, 1444 } 1445 provider.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 1446 // If we ever have more than our intended parallelism number of 1447 // plan operations running concurrently, the semaphore will fail. 1448 select { 1449 case started <- struct{}{}: 1450 defer func() { 1451 <-started 1452 }() 1453 default: 1454 t.Fatal("too many concurrent apply operations") 1455 } 1456 1457 // If we never reach our intended parallelism, the context will 1458 // never be canceled and the test will time out. 1459 if len(started) >= par { 1460 begin() 1461 } 1462 <-beginCtx.Done() 1463 1464 // do some "work" 1465 // Not required for correctness, but makes it easier to spot a 1466 // failure when there is more overlap. 1467 time.Sleep(10 * time.Millisecond) 1468 return providers.PlanResourceChangeResponse{ 1469 PlannedState: req.ProposedNewState, 1470 } 1471 } 1472 providerFactories[addrs.NewDefaultProvider(name)] = providers.FactoryFixed(provider) 1473 } 1474 testingOverrides := &testingOverrides{ 1475 Providers: providerFactories, 1476 } 1477 1478 view, done := testView(t) 1479 c := &PlanCommand{ 1480 Meta: Meta{ 1481 testingOverrides: testingOverrides, 1482 View: view, 1483 }, 1484 } 1485 1486 args := []string{ 1487 fmt.Sprintf("-parallelism=%d", par), 1488 } 1489 1490 res := c.Run(args) 1491 output := done(t) 1492 if res != 0 { 1493 t.Fatal(output.Stdout()) 1494 } 1495 } 1496 1497 func TestPlan_warnings(t *testing.T) { 1498 td := t.TempDir() 1499 testCopyDir(t, testFixturePath("plan"), td) 1500 defer testChdir(t, td)() 1501 1502 t.Run("full warnings", func(t *testing.T) { 1503 p := planWarningsFixtureProvider() 1504 view, done := testView(t) 1505 c := &PlanCommand{ 1506 Meta: Meta{ 1507 testingOverrides: metaOverridesForProvider(p), 1508 View: view, 1509 }, 1510 } 1511 code := c.Run([]string{}) 1512 output := done(t) 1513 if code != 0 { 1514 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 1515 } 1516 // the output should contain 3 warnings (returned by planWarningsFixtureProvider()) 1517 wantWarnings := []string{ 1518 "warning 1", 1519 "warning 2", 1520 "warning 3", 1521 } 1522 for _, want := range wantWarnings { 1523 if !strings.Contains(output.Stdout(), want) { 1524 t.Errorf("missing warning %s", want) 1525 } 1526 } 1527 }) 1528 1529 t.Run("compact warnings", func(t *testing.T) { 1530 p := planWarningsFixtureProvider() 1531 view, done := testView(t) 1532 c := &PlanCommand{ 1533 Meta: Meta{ 1534 testingOverrides: metaOverridesForProvider(p), 1535 View: view, 1536 }, 1537 } 1538 code := c.Run([]string{"-compact-warnings"}) 1539 output := done(t) 1540 if code != 0 { 1541 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 1542 } 1543 // the output should contain 3 warnings (returned by planWarningsFixtureProvider()) 1544 // and the message that plan was run with -compact-warnings 1545 wantWarnings := []string{ 1546 "warning 1", 1547 "warning 2", 1548 "warning 3", 1549 "To see the full warning notes, run Terraform without -compact-warnings.", 1550 } 1551 for _, want := range wantWarnings { 1552 if !strings.Contains(output.Stdout(), want) { 1553 t.Errorf("missing warning %s", want) 1554 } 1555 } 1556 }) 1557 } 1558 1559 func TestPlan_jsonGoldenReference(t *testing.T) { 1560 // Create a temporary working directory that is empty 1561 td := t.TempDir() 1562 testCopyDir(t, testFixturePath("plan"), td) 1563 defer testChdir(t, td)() 1564 1565 p := planFixtureProvider() 1566 view, done := testView(t) 1567 c := &PlanCommand{ 1568 Meta: Meta{ 1569 testingOverrides: metaOverridesForProvider(p), 1570 View: view, 1571 }, 1572 } 1573 1574 args := []string{ 1575 "-json", 1576 } 1577 code := c.Run(args) 1578 output := done(t) 1579 if code != 0 { 1580 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 1581 } 1582 1583 checkGoldenReference(t, output, "plan") 1584 } 1585 1586 // planFixtureSchema returns a schema suitable for processing the 1587 // configuration in testdata/plan . This schema should be 1588 // assigned to a mock provider named "test". 1589 func planFixtureSchema() *providers.GetProviderSchemaResponse { 1590 return &providers.GetProviderSchemaResponse{ 1591 ResourceTypes: map[string]providers.Schema{ 1592 "test_instance": { 1593 Block: &configschema.Block{ 1594 Attributes: map[string]*configschema.Attribute{ 1595 "id": {Type: cty.String, Optional: true, Computed: true}, 1596 "ami": {Type: cty.String, Optional: true}, 1597 }, 1598 BlockTypes: map[string]*configschema.NestedBlock{ 1599 "network_interface": { 1600 Nesting: configschema.NestingList, 1601 Block: configschema.Block{ 1602 Attributes: map[string]*configschema.Attribute{ 1603 "device_index": {Type: cty.String, Optional: true}, 1604 "description": {Type: cty.String, Optional: true}, 1605 }, 1606 }, 1607 }, 1608 }, 1609 }, 1610 }, 1611 }, 1612 DataSources: map[string]providers.Schema{ 1613 "test_data_source": { 1614 Block: &configschema.Block{ 1615 Attributes: map[string]*configschema.Attribute{ 1616 "id": { 1617 Type: cty.String, 1618 Required: true, 1619 }, 1620 "valid": { 1621 Type: cty.Bool, 1622 Computed: true, 1623 }, 1624 }, 1625 }, 1626 }, 1627 }, 1628 } 1629 } 1630 1631 // planFixtureProvider returns a mock provider that is configured for basic 1632 // operation with the configuration in testdata/plan. This mock has 1633 // GetSchemaResponse and PlanResourceChangeFn populated, with the plan 1634 // step just passing through the new object proposed by Terraform Core. 1635 func planFixtureProvider() *terraform.MockProvider { 1636 p := testProvider() 1637 p.GetProviderSchemaResponse = planFixtureSchema() 1638 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 1639 return providers.PlanResourceChangeResponse{ 1640 PlannedState: req.ProposedNewState, 1641 } 1642 } 1643 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 1644 return providers.ReadDataSourceResponse{ 1645 State: cty.ObjectVal(map[string]cty.Value{ 1646 "id": cty.StringVal("zzzzz"), 1647 "valid": cty.BoolVal(true), 1648 }), 1649 } 1650 } 1651 return p 1652 } 1653 1654 // planVarsFixtureSchema returns a schema suitable for processing the 1655 // configuration in testdata/plan-vars . This schema should be 1656 // assigned to a mock provider named "test". 1657 func planVarsFixtureSchema() *providers.GetProviderSchemaResponse { 1658 return &providers.GetProviderSchemaResponse{ 1659 ResourceTypes: map[string]providers.Schema{ 1660 "test_instance": { 1661 Block: &configschema.Block{ 1662 Attributes: map[string]*configschema.Attribute{ 1663 "id": {Type: cty.String, Optional: true, Computed: true}, 1664 "value": {Type: cty.String, Optional: true}, 1665 }, 1666 }, 1667 }, 1668 }, 1669 } 1670 } 1671 1672 // planVarsFixtureProvider returns a mock provider that is configured for basic 1673 // operation with the configuration in testdata/plan-vars. This mock has 1674 // GetSchemaResponse and PlanResourceChangeFn populated, with the plan 1675 // step just passing through the new object proposed by Terraform Core. 1676 func planVarsFixtureProvider() *terraform.MockProvider { 1677 p := testProvider() 1678 p.GetProviderSchemaResponse = planVarsFixtureSchema() 1679 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 1680 return providers.PlanResourceChangeResponse{ 1681 PlannedState: req.ProposedNewState, 1682 } 1683 } 1684 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 1685 return providers.ReadDataSourceResponse{ 1686 State: cty.ObjectVal(map[string]cty.Value{ 1687 "id": cty.StringVal("zzzzz"), 1688 "valid": cty.BoolVal(true), 1689 }), 1690 } 1691 } 1692 return p 1693 } 1694 1695 // planFixtureProvider returns a mock provider that is configured for basic 1696 // operation with the configuration in testdata/plan. This mock has 1697 // GetSchemaResponse and PlanResourceChangeFn populated, returning 3 warnings. 1698 func planWarningsFixtureProvider() *terraform.MockProvider { 1699 p := testProvider() 1700 p.GetProviderSchemaResponse = planFixtureSchema() 1701 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 1702 return providers.PlanResourceChangeResponse{ 1703 Diagnostics: tfdiags.Diagnostics{ 1704 tfdiags.SimpleWarning("warning 1"), 1705 tfdiags.SimpleWarning("warning 2"), 1706 tfdiags.SimpleWarning("warning 3"), 1707 }, 1708 PlannedState: req.ProposedNewState, 1709 } 1710 } 1711 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 1712 return providers.ReadDataSourceResponse{ 1713 State: cty.ObjectVal(map[string]cty.Value{ 1714 "id": cty.StringVal("zzzzz"), 1715 "valid": cty.BoolVal(true), 1716 }), 1717 } 1718 } 1719 return p 1720 } 1721 1722 const planVarFile = ` 1723 foo = "bar" 1724 ` 1725 1726 const planVarFileWithDecl = ` 1727 foo = "bar" 1728 1729 variable "nope" { 1730 } 1731 `