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