github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/command/plan_test.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "io/ioutil" 6 "os" 7 "path" 8 "path/filepath" 9 "strings" 10 "sync" 11 "testing" 12 "time" 13 14 "github.com/davecgh/go-spew/spew" 15 "github.com/zclconf/go-cty/cty" 16 17 "github.com/iaas-resource-provision/iaas-rpc/internal/addrs" 18 backendinit "github.com/iaas-resource-provision/iaas-rpc/internal/backend/init" 19 "github.com/iaas-resource-provision/iaas-rpc/internal/configs/configschema" 20 "github.com/iaas-resource-provision/iaas-rpc/internal/plans" 21 "github.com/iaas-resource-provision/iaas-rpc/internal/providers" 22 "github.com/iaas-resource-provision/iaas-rpc/internal/states" 23 "github.com/iaas-resource-provision/iaas-rpc/internal/terraform" 24 ) 25 26 func TestPlan(t *testing.T) { 27 td := tempDir(t) 28 testCopyDir(t, testFixturePath("plan"), td) 29 defer os.RemoveAll(td) 30 defer testChdir(t, td)() 31 32 p := planFixtureProvider() 33 view, done := testView(t) 34 c := &PlanCommand{ 35 Meta: Meta{ 36 testingOverrides: metaOverridesForProvider(p), 37 View: view, 38 }, 39 } 40 41 args := []string{} 42 code := c.Run(args) 43 output := done(t) 44 if code != 0 { 45 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 46 } 47 } 48 49 func TestPlan_lockedState(t *testing.T) { 50 td := tempDir(t) 51 testCopyDir(t, testFixturePath("plan"), td) 52 defer os.RemoveAll(td) 53 defer testChdir(t, td)() 54 55 unlock, err := testLockState(testDataDir, filepath.Join(td, DefaultStateFilename)) 56 if err != nil { 57 t.Fatal(err) 58 } 59 defer unlock() 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 if code == 0 { 73 t.Fatal("expected error", done(t).Stdout()) 74 } 75 76 output := done(t).Stderr() 77 if !strings.Contains(output, "lock") { 78 t.Fatal("command output does not look like a lock error:", output) 79 } 80 } 81 82 func TestPlan_plan(t *testing.T) { 83 tmp, cwd := testCwd(t) 84 defer testFixCwd(t, tmp, cwd) 85 86 planPath := testPlanFileNoop(t) 87 88 p := testProvider() 89 view, done := testView(t) 90 c := &PlanCommand{ 91 Meta: Meta{ 92 testingOverrides: metaOverridesForProvider(p), 93 View: view, 94 }, 95 } 96 97 args := []string{planPath} 98 code := c.Run(args) 99 output := done(t) 100 if code != 1 { 101 t.Fatalf("wrong exit status %d; want 1\nstderr: %s", code, output.Stderr()) 102 } 103 } 104 105 func TestPlan_destroy(t *testing.T) { 106 td := tempDir(t) 107 testCopyDir(t, testFixturePath("plan"), td) 108 defer os.RemoveAll(td) 109 defer testChdir(t, td)() 110 111 originalState := states.BuildState(func(s *states.SyncState) { 112 s.SetResourceInstanceCurrent( 113 addrs.Resource{ 114 Mode: addrs.ManagedResourceMode, 115 Type: "test_instance", 116 Name: "foo", 117 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 118 &states.ResourceInstanceObjectSrc{ 119 AttrsJSON: []byte(`{"id":"bar"}`), 120 Status: states.ObjectReady, 121 }, 122 addrs.AbsProviderConfig{ 123 Provider: addrs.NewDefaultProvider("test"), 124 Module: addrs.RootModule, 125 }, 126 ) 127 }) 128 outPath := testTempFile(t) 129 statePath := testStateFile(t, originalState) 130 131 p := planFixtureProvider() 132 view, done := testView(t) 133 c := &PlanCommand{ 134 Meta: Meta{ 135 testingOverrides: metaOverridesForProvider(p), 136 View: view, 137 }, 138 } 139 140 args := []string{ 141 "-destroy", 142 "-out", outPath, 143 "-state", statePath, 144 } 145 code := c.Run(args) 146 output := done(t) 147 if code != 0 { 148 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 149 } 150 151 plan := testReadPlan(t, outPath) 152 for _, rc := range plan.Changes.Resources { 153 if got, want := rc.Action, plans.Delete; got != want { 154 t.Fatalf("wrong action %s for %s; want %s\nplanned change: %s", got, rc.Addr, want, spew.Sdump(rc)) 155 } 156 } 157 } 158 159 func TestPlan_noState(t *testing.T) { 160 td := tempDir(t) 161 testCopyDir(t, testFixturePath("plan"), td) 162 defer os.RemoveAll(td) 163 defer testChdir(t, td)() 164 165 p := planFixtureProvider() 166 view, done := testView(t) 167 c := &PlanCommand{ 168 Meta: Meta{ 169 testingOverrides: metaOverridesForProvider(p), 170 View: view, 171 }, 172 } 173 174 args := []string{} 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 // Verify that refresh was called 182 if p.ReadResourceCalled { 183 t.Fatal("ReadResource should not be called") 184 } 185 186 // Verify that the provider was called with the existing state 187 actual := p.PlanResourceChangeRequest.PriorState 188 expected := cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["test_instance"].Block.ImpliedType()) 189 if !expected.RawEquals(actual) { 190 t.Fatalf("wrong prior state\ngot: %#v\nwant: %#v", actual, expected) 191 } 192 } 193 194 func TestPlan_outPath(t *testing.T) { 195 td := tempDir(t) 196 testCopyDir(t, testFixturePath("plan"), td) 197 defer os.RemoveAll(td) 198 defer testChdir(t, td)() 199 200 outPath := filepath.Join(td, "test.plan") 201 202 p := planFixtureProvider() 203 view, done := testView(t) 204 c := &PlanCommand{ 205 Meta: Meta{ 206 testingOverrides: metaOverridesForProvider(p), 207 View: view, 208 }, 209 } 210 211 p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{ 212 PlannedState: cty.NullVal(cty.EmptyObject), 213 } 214 215 args := []string{ 216 "-out", outPath, 217 } 218 code := c.Run(args) 219 output := done(t) 220 if code != 0 { 221 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 222 } 223 224 testReadPlan(t, outPath) // will call t.Fatal itself if the file cannot be read 225 } 226 227 func TestPlan_outPathNoChange(t *testing.T) { 228 td := tempDir(t) 229 testCopyDir(t, testFixturePath("plan"), td) 230 defer os.RemoveAll(td) 231 defer testChdir(t, td)() 232 233 originalState := states.BuildState(func(s *states.SyncState) { 234 s.SetResourceInstanceCurrent( 235 addrs.Resource{ 236 Mode: addrs.ManagedResourceMode, 237 Type: "test_instance", 238 Name: "foo", 239 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 240 &states.ResourceInstanceObjectSrc{ 241 // Aside from "id" (which is computed) the values here must 242 // exactly match the values in the "plan" test fixture in order 243 // to produce the empty plan we need for this test. 244 AttrsJSON: []byte(`{"id":"bar","ami":"bar","network_interface":[{"description":"Main network interface","device_index":"0"}]}`), 245 Status: states.ObjectReady, 246 }, 247 addrs.AbsProviderConfig{ 248 Provider: addrs.NewDefaultProvider("test"), 249 Module: addrs.RootModule, 250 }, 251 ) 252 }) 253 statePath := testStateFile(t, originalState) 254 255 outPath := filepath.Join(td, "test.plan") 256 257 p := planFixtureProvider() 258 view, done := testView(t) 259 c := &PlanCommand{ 260 Meta: Meta{ 261 testingOverrides: metaOverridesForProvider(p), 262 View: view, 263 }, 264 } 265 266 args := []string{ 267 "-out", outPath, 268 "-state", statePath, 269 } 270 code := c.Run(args) 271 output := done(t) 272 if code != 0 { 273 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 274 } 275 276 plan := testReadPlan(t, outPath) 277 if !plan.Changes.Empty() { 278 t.Fatalf("Expected empty plan to be written to plan file, got: %s", spew.Sdump(plan)) 279 } 280 } 281 282 // When using "-out" with a backend, the plan should encode the backend config 283 func TestPlan_outBackend(t *testing.T) { 284 // Create a temporary working directory that is empty 285 td := tempDir(t) 286 testCopyDir(t, testFixturePath("plan-out-backend"), td) 287 defer os.RemoveAll(td) 288 defer testChdir(t, td)() 289 290 originalState := states.BuildState(func(s *states.SyncState) { 291 s.SetResourceInstanceCurrent( 292 addrs.Resource{ 293 Mode: addrs.ManagedResourceMode, 294 Type: "test_instance", 295 Name: "foo", 296 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 297 &states.ResourceInstanceObjectSrc{ 298 AttrsJSON: []byte(`{"id":"bar","ami":"bar"}`), 299 Status: states.ObjectReady, 300 }, 301 addrs.AbsProviderConfig{ 302 Provider: addrs.NewDefaultProvider("test"), 303 Module: addrs.RootModule, 304 }, 305 ) 306 }) 307 308 // Set up our backend state 309 dataState, srv := testBackendState(t, originalState, 200) 310 defer srv.Close() 311 testStateFileRemote(t, dataState) 312 313 outPath := "foo" 314 p := testProvider() 315 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 316 ResourceTypes: map[string]providers.Schema{ 317 "test_instance": { 318 Block: &configschema.Block{ 319 Attributes: map[string]*configschema.Attribute{ 320 "id": { 321 Type: cty.String, 322 Computed: true, 323 }, 324 "ami": { 325 Type: cty.String, 326 Optional: true, 327 }, 328 }, 329 }, 330 }, 331 }, 332 } 333 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 334 return providers.PlanResourceChangeResponse{ 335 PlannedState: req.ProposedNewState, 336 } 337 } 338 view, done := testView(t) 339 c := &PlanCommand{ 340 Meta: Meta{ 341 testingOverrides: metaOverridesForProvider(p), 342 View: view, 343 }, 344 } 345 346 args := []string{ 347 "-out", outPath, 348 } 349 code := c.Run(args) 350 output := done(t) 351 if code != 0 { 352 t.Logf("stdout: %s", output.Stdout()) 353 t.Fatalf("plan command failed with exit code %d\n\n%s", code, output.Stderr()) 354 } 355 356 plan := testReadPlan(t, outPath) 357 if !plan.Changes.Empty() { 358 t.Fatalf("Expected empty plan to be written to plan file, got: %s", spew.Sdump(plan)) 359 } 360 361 if got, want := plan.Backend.Type, "http"; got != want { 362 t.Errorf("wrong backend type %q; want %q", got, want) 363 } 364 if got, want := plan.Backend.Workspace, "default"; got != want { 365 t.Errorf("wrong backend workspace %q; want %q", got, want) 366 } 367 { 368 httpBackend := backendinit.Backend("http")() 369 schema := httpBackend.ConfigSchema() 370 got, err := plan.Backend.Config.Decode(schema.ImpliedType()) 371 if err != nil { 372 t.Fatalf("failed to decode backend config in plan: %s", err) 373 } 374 want, err := dataState.Backend.Config(schema) 375 if err != nil { 376 t.Fatalf("failed to decode cached config: %s", err) 377 } 378 if !want.RawEquals(got) { 379 t.Errorf("wrong backend config\ngot: %#v\nwant: %#v", got, want) 380 } 381 } 382 } 383 384 func TestPlan_refreshFalse(t *testing.T) { 385 // Create a temporary working directory that is empty 386 td := tempDir(t) 387 testCopyDir(t, testFixturePath("plan"), td) 388 defer os.RemoveAll(td) 389 defer testChdir(t, td)() 390 391 p := planFixtureProvider() 392 view, done := testView(t) 393 c := &PlanCommand{ 394 Meta: Meta{ 395 testingOverrides: metaOverridesForProvider(p), 396 View: view, 397 }, 398 } 399 400 args := []string{ 401 "-refresh=false", 402 } 403 code := c.Run(args) 404 output := done(t) 405 if code != 0 { 406 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 407 } 408 409 if p.ReadResourceCalled { 410 t.Fatal("ReadResource should not have been called") 411 } 412 } 413 414 func TestPlan_state(t *testing.T) { 415 // Create a temporary working directory that is empty 416 td := tempDir(t) 417 testCopyDir(t, testFixturePath("plan"), td) 418 defer os.RemoveAll(td) 419 defer testChdir(t, td)() 420 421 originalState := testState() 422 statePath := testStateFile(t, originalState) 423 424 p := planFixtureProvider() 425 view, done := testView(t) 426 c := &PlanCommand{ 427 Meta: Meta{ 428 testingOverrides: metaOverridesForProvider(p), 429 View: view, 430 }, 431 } 432 433 args := []string{ 434 "-state", statePath, 435 } 436 code := c.Run(args) 437 output := done(t) 438 if code != 0 { 439 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 440 } 441 442 // Verify that the provider was called with the existing state 443 actual := p.PlanResourceChangeRequest.PriorState 444 expected := cty.ObjectVal(map[string]cty.Value{ 445 "id": cty.StringVal("bar"), 446 "ami": cty.NullVal(cty.String), 447 "network_interface": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ 448 "device_index": cty.String, 449 "description": cty.String, 450 }))), 451 }) 452 if !expected.RawEquals(actual) { 453 t.Fatalf("wrong prior state\ngot: %#v\nwant: %#v", actual, expected) 454 } 455 } 456 457 func TestPlan_stateDefault(t *testing.T) { 458 // Create a temporary working directory that is empty 459 td := tempDir(t) 460 testCopyDir(t, testFixturePath("plan"), td) 461 defer os.RemoveAll(td) 462 defer testChdir(t, td)() 463 464 // Generate state and move it to the default path 465 originalState := testState() 466 statePath := testStateFile(t, originalState) 467 os.Rename(statePath, path.Join(td, "resource_state.json")) 468 469 p := planFixtureProvider() 470 view, done := testView(t) 471 c := &PlanCommand{ 472 Meta: Meta{ 473 testingOverrides: metaOverridesForProvider(p), 474 View: view, 475 }, 476 } 477 478 args := []string{} 479 code := c.Run(args) 480 output := done(t) 481 if code != 0 { 482 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 483 } 484 485 // Verify that the provider was called with the existing state 486 actual := p.PlanResourceChangeRequest.PriorState 487 expected := cty.ObjectVal(map[string]cty.Value{ 488 "id": cty.StringVal("bar"), 489 "ami": cty.NullVal(cty.String), 490 "network_interface": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ 491 "device_index": cty.String, 492 "description": cty.String, 493 }))), 494 }) 495 if !expected.RawEquals(actual) { 496 t.Fatalf("wrong prior state\ngot: %#v\nwant: %#v", actual, expected) 497 } 498 } 499 500 func TestPlan_validate(t *testing.T) { 501 // This is triggered by not asking for input so we have to set this to false 502 test = false 503 defer func() { test = true }() 504 505 td := tempDir(t) 506 testCopyDir(t, testFixturePath("plan-invalid"), td) 507 defer os.RemoveAll(td) 508 defer testChdir(t, td)() 509 510 p := testProvider() 511 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 512 ResourceTypes: map[string]providers.Schema{ 513 "test_instance": { 514 Block: &configschema.Block{ 515 Attributes: map[string]*configschema.Attribute{ 516 "id": {Type: cty.String, Optional: true, Computed: true}, 517 }, 518 }, 519 }, 520 }, 521 } 522 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 523 return providers.PlanResourceChangeResponse{ 524 PlannedState: req.ProposedNewState, 525 } 526 } 527 view, done := testView(t) 528 c := &PlanCommand{ 529 Meta: Meta{ 530 testingOverrides: metaOverridesForProvider(p), 531 View: view, 532 }, 533 } 534 535 args := []string{"-no-color"} 536 code := c.Run(args) 537 output := done(t) 538 if code != 1 { 539 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 540 } 541 542 actual := output.Stderr() 543 if want := "Error: Invalid count argument"; !strings.Contains(actual, want) { 544 t.Fatalf("unexpected error output\ngot:\n%s\n\nshould contain: %s", actual, want) 545 } 546 if want := "9: count = timestamp()"; !strings.Contains(actual, want) { 547 t.Fatalf("unexpected error output\ngot:\n%s\n\nshould contain: %s", actual, want) 548 } 549 } 550 551 func TestPlan_vars(t *testing.T) { 552 // Create a temporary working directory that is empty 553 td := tempDir(t) 554 testCopyDir(t, testFixturePath("plan-vars"), td) 555 defer os.RemoveAll(td) 556 defer testChdir(t, td)() 557 558 p := planVarsFixtureProvider() 559 view, done := testView(t) 560 c := &PlanCommand{ 561 Meta: Meta{ 562 testingOverrides: metaOverridesForProvider(p), 563 View: view, 564 }, 565 } 566 567 actual := "" 568 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 569 actual = req.ProposedNewState.GetAttr("value").AsString() 570 resp.PlannedState = req.ProposedNewState 571 return 572 } 573 574 args := []string{ 575 "-var", "foo=bar", 576 } 577 code := c.Run(args) 578 output := done(t) 579 if code != 0 { 580 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 581 } 582 583 if actual != "bar" { 584 t.Fatal("didn't work") 585 } 586 } 587 588 func TestPlan_varsUnset(t *testing.T) { 589 // Create a temporary working directory that is empty 590 td := tempDir(t) 591 testCopyDir(t, testFixturePath("plan-vars"), td) 592 defer os.RemoveAll(td) 593 defer testChdir(t, td)() 594 595 // The plan command will prompt for interactive input of var.foo. 596 // We'll answer "bar" to that prompt, which should then allow this 597 // configuration to apply even though var.foo doesn't have a 598 // default value and there are no -var arguments on our command line. 599 600 // This will (helpfully) panic if more than one variable is requested during plan: 601 // https://github.com/iaas-resource-provision/iaas-rpc/issues/26027 602 close := testInteractiveInput(t, []string{"bar"}) 603 defer close() 604 605 p := planVarsFixtureProvider() 606 view, done := testView(t) 607 c := &PlanCommand{ 608 Meta: Meta{ 609 testingOverrides: metaOverridesForProvider(p), 610 View: view, 611 }, 612 } 613 614 args := []string{} 615 code := c.Run(args) 616 output := done(t) 617 if code != 0 { 618 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 619 } 620 } 621 622 // This test adds a required argument to the test provider to validate 623 // processing of user input: 624 // https://github.com/iaas-resource-provision/iaas-rpc/issues/26035 625 func TestPlan_providerArgumentUnset(t *testing.T) { 626 // Create a temporary working directory that is empty 627 td := tempDir(t) 628 testCopyDir(t, testFixturePath("plan"), td) 629 defer os.RemoveAll(td) 630 defer testChdir(t, td)() 631 632 // Disable test mode so input would be asked 633 test = false 634 defer func() { test = true }() 635 636 // The plan command will prompt for interactive input of provider.test.region 637 defaultInputReader = bytes.NewBufferString("us-east-1\n") 638 639 p := planFixtureProvider() 640 // override the planFixtureProvider schema to include a required provider argument 641 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 642 Provider: providers.Schema{ 643 Block: &configschema.Block{ 644 Attributes: map[string]*configschema.Attribute{ 645 "region": {Type: cty.String, Required: true}, 646 }, 647 }, 648 }, 649 ResourceTypes: map[string]providers.Schema{ 650 "test_instance": { 651 Block: &configschema.Block{ 652 Attributes: map[string]*configschema.Attribute{ 653 "id": {Type: cty.String, Optional: true, Computed: true}, 654 "ami": {Type: cty.String, Optional: true, Computed: true}, 655 }, 656 BlockTypes: map[string]*configschema.NestedBlock{ 657 "network_interface": { 658 Nesting: configschema.NestingList, 659 Block: configschema.Block{ 660 Attributes: map[string]*configschema.Attribute{ 661 "device_index": {Type: cty.String, Optional: true}, 662 "description": {Type: cty.String, Optional: true}, 663 }, 664 }, 665 }, 666 }, 667 }, 668 }, 669 }, 670 } 671 view, done := testView(t) 672 c := &PlanCommand{ 673 Meta: Meta{ 674 testingOverrides: metaOverridesForProvider(p), 675 View: view, 676 }, 677 } 678 679 args := []string{} 680 code := c.Run(args) 681 output := done(t) 682 if code != 0 { 683 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 684 } 685 } 686 687 func TestPlan_varFile(t *testing.T) { 688 // Create a temporary working directory that is empty 689 td := tempDir(t) 690 testCopyDir(t, testFixturePath("plan-vars"), td) 691 defer os.RemoveAll(td) 692 defer testChdir(t, td)() 693 694 varFilePath := testTempFile(t) 695 if err := ioutil.WriteFile(varFilePath, []byte(planVarFile), 0644); err != nil { 696 t.Fatalf("err: %s", err) 697 } 698 699 p := planVarsFixtureProvider() 700 view, done := testView(t) 701 c := &PlanCommand{ 702 Meta: Meta{ 703 testingOverrides: metaOverridesForProvider(p), 704 View: view, 705 }, 706 } 707 708 actual := "" 709 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 710 actual = req.ProposedNewState.GetAttr("value").AsString() 711 resp.PlannedState = req.ProposedNewState 712 return 713 } 714 715 args := []string{ 716 "-var-file", varFilePath, 717 } 718 code := c.Run(args) 719 output := done(t) 720 if code != 0 { 721 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 722 } 723 724 if actual != "bar" { 725 t.Fatal("didn't work") 726 } 727 } 728 729 func TestPlan_varFileDefault(t *testing.T) { 730 // Create a temporary working directory that is empty 731 td := tempDir(t) 732 testCopyDir(t, testFixturePath("plan-vars"), td) 733 defer os.RemoveAll(td) 734 defer testChdir(t, td)() 735 736 varFilePath := filepath.Join(td, "terraform.tfvars") 737 if err := ioutil.WriteFile(varFilePath, []byte(planVarFile), 0644); err != nil { 738 t.Fatalf("err: %s", err) 739 } 740 741 p := planVarsFixtureProvider() 742 view, done := testView(t) 743 c := &PlanCommand{ 744 Meta: Meta{ 745 testingOverrides: metaOverridesForProvider(p), 746 View: view, 747 }, 748 } 749 750 actual := "" 751 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 752 actual = req.ProposedNewState.GetAttr("value").AsString() 753 resp.PlannedState = req.ProposedNewState 754 return 755 } 756 757 args := []string{} 758 code := c.Run(args) 759 output := done(t) 760 if code != 0 { 761 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 762 } 763 764 if actual != "bar" { 765 t.Fatal("didn't work") 766 } 767 } 768 769 func TestPlan_varFileWithDecls(t *testing.T) { 770 // Create a temporary working directory that is empty 771 td := tempDir(t) 772 testCopyDir(t, testFixturePath("plan-vars"), td) 773 defer os.RemoveAll(td) 774 defer testChdir(t, td)() 775 776 varFilePath := testTempFile(t) 777 if err := ioutil.WriteFile(varFilePath, []byte(planVarFileWithDecl), 0644); err != nil { 778 t.Fatalf("err: %s", err) 779 } 780 781 p := planVarsFixtureProvider() 782 view, done := testView(t) 783 c := &PlanCommand{ 784 Meta: Meta{ 785 testingOverrides: metaOverridesForProvider(p), 786 View: view, 787 }, 788 } 789 790 args := []string{ 791 "-var-file", varFilePath, 792 } 793 code := c.Run(args) 794 output := done(t) 795 if code == 0 { 796 t.Fatalf("succeeded; want failure\n\n%s", output.Stdout()) 797 } 798 799 msg := output.Stderr() 800 if got, want := msg, "Variable declaration in .tfvars file"; !strings.Contains(got, want) { 801 t.Fatalf("missing expected error message\nwant message containing %q\ngot:\n%s", want, got) 802 } 803 } 804 805 func TestPlan_detailedExitcode(t *testing.T) { 806 td := tempDir(t) 807 testCopyDir(t, testFixturePath("plan"), td) 808 defer os.RemoveAll(td) 809 defer testChdir(t, td)() 810 811 p := planFixtureProvider() 812 view, done := testView(t) 813 c := &PlanCommand{ 814 Meta: Meta{ 815 testingOverrides: metaOverridesForProvider(p), 816 View: view, 817 }, 818 } 819 820 args := []string{"-detailed-exitcode"} 821 code := c.Run(args) 822 output := done(t) 823 if code != 2 { 824 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 825 } 826 } 827 828 func TestPlan_detailedExitcode_emptyDiff(t *testing.T) { 829 td := tempDir(t) 830 testCopyDir(t, testFixturePath("plan-emptydiff"), td) 831 defer os.RemoveAll(td) 832 defer testChdir(t, td)() 833 834 p := testProvider() 835 view, done := testView(t) 836 c := &PlanCommand{ 837 Meta: Meta{ 838 testingOverrides: metaOverridesForProvider(p), 839 View: view, 840 }, 841 } 842 843 args := []string{"-detailed-exitcode"} 844 code := c.Run(args) 845 output := done(t) 846 if code != 0 { 847 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 848 } 849 } 850 851 func TestPlan_shutdown(t *testing.T) { 852 // Create a temporary working directory that is empty 853 td := tempDir(t) 854 testCopyDir(t, testFixturePath("apply-shutdown"), td) 855 defer os.RemoveAll(td) 856 defer testChdir(t, td)() 857 858 cancelled := make(chan struct{}) 859 shutdownCh := make(chan struct{}) 860 861 p := testProvider() 862 view, done := testView(t) 863 c := &PlanCommand{ 864 Meta: Meta{ 865 testingOverrides: metaOverridesForProvider(p), 866 View: view, 867 ShutdownCh: shutdownCh, 868 }, 869 } 870 871 p.StopFn = func() error { 872 close(cancelled) 873 return nil 874 } 875 876 var once sync.Once 877 878 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 879 once.Do(func() { 880 shutdownCh <- struct{}{} 881 }) 882 883 // Because of the internal lock in the MockProvider, we can't 884 // coordinate directly with the calling of Stop, and making the 885 // MockProvider concurrent is disruptive to a lot of existing tests. 886 // Wait here a moment to help make sure the main goroutine gets to the 887 // Stop call before we exit, or the plan may finish before it can be 888 // canceled. 889 time.Sleep(200 * time.Millisecond) 890 891 s := req.ProposedNewState.AsValueMap() 892 s["ami"] = cty.StringVal("bar") 893 resp.PlannedState = cty.ObjectVal(s) 894 return 895 } 896 897 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 898 ResourceTypes: map[string]providers.Schema{ 899 "test_instance": { 900 Block: &configschema.Block{ 901 Attributes: map[string]*configschema.Attribute{ 902 "ami": {Type: cty.String, Optional: true}, 903 }, 904 }, 905 }, 906 }, 907 } 908 909 code := c.Run([]string{}) 910 output := done(t) 911 if code != 1 { 912 t.Errorf("wrong exit code %d; want 1\noutput:\n%s", code, output.Stdout()) 913 } 914 915 select { 916 case <-cancelled: 917 default: 918 t.Error("command not cancelled") 919 } 920 } 921 922 func TestPlan_init_required(t *testing.T) { 923 td := tempDir(t) 924 testCopyDir(t, testFixturePath("plan"), td) 925 defer os.RemoveAll(td) 926 defer testChdir(t, td)() 927 928 view, done := testView(t) 929 c := &PlanCommand{ 930 Meta: Meta{ 931 // Running plan without setting testingOverrides is similar to plan without init 932 View: view, 933 }, 934 } 935 936 args := []string{} 937 code := c.Run(args) 938 output := done(t) 939 if code != 1 { 940 t.Fatalf("expected error, got success") 941 } 942 got := output.Stderr() 943 if !strings.Contains(got, `Plugin reinitialization required. Please run "terraform init".`) { 944 t.Fatal("wrong error message in output:", got) 945 } 946 } 947 948 // Config with multiple resources, targeting plan of a subset 949 func TestPlan_targeted(t *testing.T) { 950 td := tempDir(t) 951 testCopyDir(t, testFixturePath("apply-targeted"), td) 952 defer os.RemoveAll(td) 953 defer testChdir(t, td)() 954 955 p := testProvider() 956 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 957 ResourceTypes: map[string]providers.Schema{ 958 "test_instance": { 959 Block: &configschema.Block{ 960 Attributes: map[string]*configschema.Attribute{ 961 "id": {Type: cty.String, Computed: true}, 962 }, 963 }, 964 }, 965 }, 966 } 967 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 968 return providers.PlanResourceChangeResponse{ 969 PlannedState: req.ProposedNewState, 970 } 971 } 972 973 view, done := testView(t) 974 c := &PlanCommand{ 975 Meta: Meta{ 976 testingOverrides: metaOverridesForProvider(p), 977 View: view, 978 }, 979 } 980 981 args := []string{ 982 "-target", "test_instance.foo", 983 "-target", "test_instance.baz", 984 } 985 code := c.Run(args) 986 output := done(t) 987 if code != 0 { 988 t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) 989 } 990 991 if got, want := output.Stdout(), "3 to add, 0 to change, 0 to destroy"; !strings.Contains(got, want) { 992 t.Fatalf("bad change summary, want %q, got:\n%s", want, got) 993 } 994 } 995 996 // Diagnostics for invalid -target flags 997 func TestPlan_targetFlagsDiags(t *testing.T) { 998 testCases := map[string]string{ 999 "test_instance.": "Dot must be followed by attribute name.", 1000 "test_instance": "Resource specification must include a resource type and name.", 1001 } 1002 1003 for target, wantDiag := range testCases { 1004 t.Run(target, func(t *testing.T) { 1005 td := testTempDir(t) 1006 defer os.RemoveAll(td) 1007 defer testChdir(t, td)() 1008 1009 view, done := testView(t) 1010 c := &PlanCommand{ 1011 Meta: Meta{ 1012 View: view, 1013 }, 1014 } 1015 1016 args := []string{ 1017 "-target", target, 1018 } 1019 code := c.Run(args) 1020 output := done(t) 1021 if code != 1 { 1022 t.Fatalf("bad: %d\n\n%s", code, output.Stdout()) 1023 } 1024 1025 got := output.Stderr() 1026 if !strings.Contains(got, target) { 1027 t.Fatalf("bad error output, want %q, got:\n%s", target, got) 1028 } 1029 if !strings.Contains(got, wantDiag) { 1030 t.Fatalf("bad error output, want %q, got:\n%s", wantDiag, got) 1031 } 1032 }) 1033 } 1034 } 1035 1036 func TestPlan_replace(t *testing.T) { 1037 td := tempDir(t) 1038 testCopyDir(t, testFixturePath("plan-replace"), td) 1039 defer os.RemoveAll(td) 1040 defer testChdir(t, td)() 1041 1042 originalState := states.BuildState(func(s *states.SyncState) { 1043 s.SetResourceInstanceCurrent( 1044 addrs.Resource{ 1045 Mode: addrs.ManagedResourceMode, 1046 Type: "test_instance", 1047 Name: "a", 1048 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 1049 &states.ResourceInstanceObjectSrc{ 1050 AttrsJSON: []byte(`{"id":"hello"}`), 1051 Status: states.ObjectReady, 1052 }, 1053 addrs.AbsProviderConfig{ 1054 Provider: addrs.NewDefaultProvider("test"), 1055 Module: addrs.RootModule, 1056 }, 1057 ) 1058 }) 1059 statePath := testStateFile(t, originalState) 1060 1061 p := testProvider() 1062 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 1063 ResourceTypes: map[string]providers.Schema{ 1064 "test_instance": { 1065 Block: &configschema.Block{ 1066 Attributes: map[string]*configschema.Attribute{ 1067 "id": {Type: cty.String, Computed: true}, 1068 }, 1069 }, 1070 }, 1071 }, 1072 } 1073 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 1074 return providers.PlanResourceChangeResponse{ 1075 PlannedState: req.ProposedNewState, 1076 } 1077 } 1078 1079 view, done := testView(t) 1080 c := &PlanCommand{ 1081 Meta: Meta{ 1082 testingOverrides: metaOverridesForProvider(p), 1083 View: view, 1084 }, 1085 } 1086 1087 args := []string{ 1088 "-state", statePath, 1089 "-no-color", 1090 "-replace", "test_instance.a", 1091 } 1092 code := c.Run(args) 1093 output := done(t) 1094 if code != 0 { 1095 t.Fatalf("wrong exit code %d\n\n%s", code, output.Stderr()) 1096 } 1097 1098 stdout := output.Stdout() 1099 if got, want := stdout, "1 to add, 0 to change, 1 to destroy"; !strings.Contains(got, want) { 1100 t.Errorf("wrong plan summary\ngot output:\n%s\n\nwant substring: %s", got, want) 1101 } 1102 if got, want := stdout, "test_instance.a will be replaced, as requested"; !strings.Contains(got, want) { 1103 t.Errorf("missing replace explanation\ngot output:\n%s\n\nwant substring: %s", got, want) 1104 } 1105 1106 } 1107 1108 // planFixtureSchema returns a schema suitable for processing the 1109 // configuration in testdata/plan . This schema should be 1110 // assigned to a mock provider named "test". 1111 func planFixtureSchema() *providers.GetProviderSchemaResponse { 1112 return &providers.GetProviderSchemaResponse{ 1113 ResourceTypes: map[string]providers.Schema{ 1114 "test_instance": { 1115 Block: &configschema.Block{ 1116 Attributes: map[string]*configschema.Attribute{ 1117 "id": {Type: cty.String, Optional: true, Computed: true}, 1118 "ami": {Type: cty.String, Optional: true}, 1119 }, 1120 BlockTypes: map[string]*configschema.NestedBlock{ 1121 "network_interface": { 1122 Nesting: configschema.NestingList, 1123 Block: configschema.Block{ 1124 Attributes: map[string]*configschema.Attribute{ 1125 "device_index": {Type: cty.String, Optional: true}, 1126 "description": {Type: cty.String, Optional: true}, 1127 }, 1128 }, 1129 }, 1130 }, 1131 }, 1132 }, 1133 }, 1134 } 1135 } 1136 1137 // planFixtureProvider returns a mock provider that is configured for basic 1138 // operation with the configuration in testdata/plan. This mock has 1139 // GetSchemaResponse and PlanResourceChangeFn populated, with the plan 1140 // step just passing through the new object proposed by Terraform Core. 1141 func planFixtureProvider() *terraform.MockProvider { 1142 p := testProvider() 1143 p.GetProviderSchemaResponse = planFixtureSchema() 1144 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 1145 return providers.PlanResourceChangeResponse{ 1146 PlannedState: req.ProposedNewState, 1147 } 1148 } 1149 return p 1150 } 1151 1152 // planVarsFixtureSchema returns a schema suitable for processing the 1153 // configuration in testdata/plan-vars . This schema should be 1154 // assigned to a mock provider named "test". 1155 func planVarsFixtureSchema() *providers.GetProviderSchemaResponse { 1156 return &providers.GetProviderSchemaResponse{ 1157 ResourceTypes: map[string]providers.Schema{ 1158 "test_instance": { 1159 Block: &configschema.Block{ 1160 Attributes: map[string]*configschema.Attribute{ 1161 "id": {Type: cty.String, Optional: true, Computed: true}, 1162 "value": {Type: cty.String, Optional: true}, 1163 }, 1164 }, 1165 }, 1166 }, 1167 } 1168 } 1169 1170 // planVarsFixtureProvider returns a mock provider that is configured for basic 1171 // operation with the configuration in testdata/plan-vars. This mock has 1172 // GetSchemaResponse and PlanResourceChangeFn populated, with the plan 1173 // step just passing through the new object proposed by Terraform Core. 1174 func planVarsFixtureProvider() *terraform.MockProvider { 1175 p := testProvider() 1176 p.GetProviderSchemaResponse = planVarsFixtureSchema() 1177 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 1178 return providers.PlanResourceChangeResponse{ 1179 PlannedState: req.ProposedNewState, 1180 } 1181 } 1182 return p 1183 } 1184 1185 const planVarFile = ` 1186 foo = "bar" 1187 ` 1188 1189 const planVarFileWithDecl = ` 1190 foo = "bar" 1191 1192 variable "nope" { 1193 } 1194 `