github.com/hugorut/terraform@v1.1.3/src/backend/local/backend_plan_test.go (about) 1 package local 2 3 import ( 4 "context" 5 "os" 6 "path/filepath" 7 "strings" 8 "testing" 9 10 "github.com/hugorut/terraform/src/addrs" 11 "github.com/hugorut/terraform/src/backend" 12 "github.com/hugorut/terraform/src/command/arguments" 13 "github.com/hugorut/terraform/src/command/clistate" 14 "github.com/hugorut/terraform/src/command/views" 15 "github.com/hugorut/terraform/src/configs/configschema" 16 "github.com/hugorut/terraform/src/depsfile" 17 "github.com/hugorut/terraform/src/initwd" 18 "github.com/hugorut/terraform/src/plans" 19 "github.com/hugorut/terraform/src/plans/planfile" 20 "github.com/hugorut/terraform/src/states" 21 "github.com/hugorut/terraform/src/terminal" 22 "github.com/hugorut/terraform/src/terraform" 23 "github.com/zclconf/go-cty/cty" 24 ) 25 26 func TestLocal_planBasic(t *testing.T) { 27 b := TestLocal(t) 28 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 29 30 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 31 defer configCleanup() 32 op.PlanRefresh = true 33 34 run, err := b.Operation(context.Background(), op) 35 if err != nil { 36 t.Fatalf("bad: %s", err) 37 } 38 <-run.Done() 39 if run.Result != backend.OperationSuccess { 40 t.Fatalf("plan operation failed") 41 } 42 43 if !p.PlanResourceChangeCalled { 44 t.Fatal("PlanResourceChange should be called") 45 } 46 47 // the backend should be unlocked after a run 48 assertBackendStateUnlocked(t, b) 49 50 if errOutput := done(t).Stderr(); errOutput != "" { 51 t.Fatalf("unexpected error output:\n%s", errOutput) 52 } 53 } 54 55 func TestLocal_planInAutomation(t *testing.T) { 56 b := TestLocal(t) 57 TestLocalProvider(t, b, "test", planFixtureSchema()) 58 59 const msg = `You didn't use the -out option` 60 61 // When we're "in automation" we omit certain text from the plan output. 62 // However, the responsibility for this omission is in the view, so here we 63 // test for its presence while the "in automation" setting is false, to 64 // validate that we are calling the correct view method. 65 // 66 // Ideally this test would be replaced by a call-logging mock view, but 67 // that's future work. 68 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 69 defer configCleanup() 70 op.PlanRefresh = true 71 72 run, err := b.Operation(context.Background(), op) 73 if err != nil { 74 t.Fatalf("unexpected error: %s", err) 75 } 76 <-run.Done() 77 if run.Result != backend.OperationSuccess { 78 t.Fatalf("plan operation failed") 79 } 80 81 if output := done(t).Stdout(); !strings.Contains(output, msg) { 82 t.Fatalf("missing next-steps message when not in automation\nwant: %s\noutput:\n%s", msg, output) 83 } 84 } 85 86 func TestLocal_planNoConfig(t *testing.T) { 87 b := TestLocal(t) 88 TestLocalProvider(t, b, "test", &terraform.ProviderSchema{}) 89 90 op, configCleanup, done := testOperationPlan(t, "./testdata/empty") 91 defer configCleanup() 92 op.PlanRefresh = true 93 94 run, err := b.Operation(context.Background(), op) 95 if err != nil { 96 t.Fatalf("bad: %s", err) 97 } 98 <-run.Done() 99 100 output := done(t) 101 102 if run.Result == backend.OperationSuccess { 103 t.Fatal("plan operation succeeded; want failure") 104 } 105 106 if stderr := output.Stderr(); !strings.Contains(stderr, "No configuration files") { 107 t.Fatalf("bad: %s", stderr) 108 } 109 110 // the backend should be unlocked after a run 111 assertBackendStateUnlocked(t, b) 112 } 113 114 // This test validates the state lacking behavior when the inner call to 115 // Context() fails 116 func TestLocal_plan_context_error(t *testing.T) { 117 b := TestLocal(t) 118 119 // This is an intentionally-invalid value to make terraform.NewContext fail 120 // when b.Operation calls it. 121 // NOTE: This test was originally using a provider initialization failure 122 // as its forced error condition, but terraform.NewContext is no longer 123 // responsible for checking that. Invalid parallelism is the last situation 124 // where terraform.NewContext can return error diagnostics, and arguably 125 // we should be validating this argument at the UI layer anyway, so perhaps 126 // in future we'll make terraform.NewContext never return errors and then 127 // this test will become redundant, because its purpose is specifically 128 // to test that we properly unlock the state if terraform.NewContext 129 // returns an error. 130 if b.ContextOpts == nil { 131 b.ContextOpts = &terraform.ContextOpts{} 132 } 133 b.ContextOpts.Parallelism = -1 134 135 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 136 defer configCleanup() 137 138 // we coerce a failure in Context() by omitting the provider schema 139 run, err := b.Operation(context.Background(), op) 140 if err != nil { 141 t.Fatalf("bad: %s", err) 142 } 143 <-run.Done() 144 if run.Result != backend.OperationFailure { 145 t.Fatalf("plan operation succeeded") 146 } 147 148 // the backend should be unlocked after a run 149 assertBackendStateUnlocked(t, b) 150 151 if got, want := done(t).Stderr(), "Error: Invalid parallelism value"; !strings.Contains(got, want) { 152 t.Fatalf("unexpected error output:\n%s\nwant: %s", got, want) 153 } 154 } 155 156 func TestLocal_planOutputsChanged(t *testing.T) { 157 b := TestLocal(t) 158 testStateFile(t, b.StatePath, states.BuildState(func(ss *states.SyncState) { 159 ss.SetOutputValue(addrs.AbsOutputValue{ 160 Module: addrs.RootModuleInstance, 161 OutputValue: addrs.OutputValue{Name: "changed"}, 162 }, cty.StringVal("before"), false) 163 ss.SetOutputValue(addrs.AbsOutputValue{ 164 Module: addrs.RootModuleInstance, 165 OutputValue: addrs.OutputValue{Name: "sensitive_before"}, 166 }, cty.StringVal("before"), true) 167 ss.SetOutputValue(addrs.AbsOutputValue{ 168 Module: addrs.RootModuleInstance, 169 OutputValue: addrs.OutputValue{Name: "sensitive_after"}, 170 }, cty.StringVal("before"), false) 171 ss.SetOutputValue(addrs.AbsOutputValue{ 172 Module: addrs.RootModuleInstance, 173 OutputValue: addrs.OutputValue{Name: "removed"}, // not present in the config fixture 174 }, cty.StringVal("before"), false) 175 ss.SetOutputValue(addrs.AbsOutputValue{ 176 Module: addrs.RootModuleInstance, 177 OutputValue: addrs.OutputValue{Name: "unchanged"}, 178 }, cty.StringVal("before"), false) 179 // NOTE: This isn't currently testing the situation where the new 180 // value of an output is unknown, because to do that requires there to 181 // be at least one managed resource Create action in the plan and that 182 // would defeat the point of this test, which is to ensure that a 183 // plan containing only output changes is considered "non-empty". 184 // For now we're not too worried about testing the "new value is 185 // unknown" situation because that's already common for printing out 186 // resource changes and we already have many tests for that. 187 })) 188 outDir := t.TempDir() 189 defer os.RemoveAll(outDir) 190 planPath := filepath.Join(outDir, "plan.tfplan") 191 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-outputs-changed") 192 defer configCleanup() 193 op.PlanRefresh = true 194 op.PlanOutPath = planPath 195 cfg := cty.ObjectVal(map[string]cty.Value{ 196 "path": cty.StringVal(b.StatePath), 197 }) 198 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 199 if err != nil { 200 t.Fatal(err) 201 } 202 op.PlanOutBackend = &plans.Backend{ 203 // Just a placeholder so that we can generate a valid plan file. 204 Type: "local", 205 Config: cfgRaw, 206 } 207 run, err := b.Operation(context.Background(), op) 208 if err != nil { 209 t.Fatalf("bad: %s", err) 210 } 211 <-run.Done() 212 if run.Result != backend.OperationSuccess { 213 t.Fatalf("plan operation failed") 214 } 215 if run.PlanEmpty { 216 t.Error("plan should not be empty") 217 } 218 219 expectedOutput := strings.TrimSpace(` 220 Changes to Outputs: 221 + added = "after" 222 ~ changed = "before" -> "after" 223 - removed = "before" -> null 224 ~ sensitive_after = (sensitive value) 225 ~ sensitive_before = (sensitive value) 226 227 You can apply this plan to save these new output values to the Terraform 228 state, without changing any real infrastructure. 229 `) 230 231 if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) { 232 t.Errorf("Unexpected output:\n%s\n\nwant output containing:\n%s", output, expectedOutput) 233 } 234 } 235 236 // Module outputs should not cause the plan to be rendered 237 func TestLocal_planModuleOutputsChanged(t *testing.T) { 238 b := TestLocal(t) 239 testStateFile(t, b.StatePath, states.BuildState(func(ss *states.SyncState) { 240 ss.SetOutputValue(addrs.AbsOutputValue{ 241 Module: addrs.RootModuleInstance.Child("mod", addrs.NoKey), 242 OutputValue: addrs.OutputValue{Name: "changed"}, 243 }, cty.StringVal("before"), false) 244 })) 245 outDir := t.TempDir() 246 defer os.RemoveAll(outDir) 247 planPath := filepath.Join(outDir, "plan.tfplan") 248 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-module-outputs-changed") 249 defer configCleanup() 250 op.PlanRefresh = true 251 op.PlanOutPath = planPath 252 cfg := cty.ObjectVal(map[string]cty.Value{ 253 "path": cty.StringVal(b.StatePath), 254 }) 255 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 256 if err != nil { 257 t.Fatal(err) 258 } 259 op.PlanOutBackend = &plans.Backend{ 260 Type: "local", 261 Config: cfgRaw, 262 } 263 run, err := b.Operation(context.Background(), op) 264 if err != nil { 265 t.Fatalf("bad: %s", err) 266 } 267 <-run.Done() 268 if run.Result != backend.OperationSuccess { 269 t.Fatalf("plan operation failed") 270 } 271 if !run.PlanEmpty { 272 t.Fatal("plan should be empty") 273 } 274 275 expectedOutput := strings.TrimSpace(` 276 No changes. Your infrastructure matches the configuration. 277 `) 278 if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) { 279 t.Fatalf("Unexpected output:\n%s\n\nwant output containing:\n%s", output, expectedOutput) 280 } 281 } 282 283 func TestLocal_planTainted(t *testing.T) { 284 b := TestLocal(t) 285 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 286 testStateFile(t, b.StatePath, testPlanState_tainted()) 287 outDir := t.TempDir() 288 planPath := filepath.Join(outDir, "plan.tfplan") 289 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 290 defer configCleanup() 291 op.PlanRefresh = true 292 op.PlanOutPath = planPath 293 cfg := cty.ObjectVal(map[string]cty.Value{ 294 "path": cty.StringVal(b.StatePath), 295 }) 296 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 297 if err != nil { 298 t.Fatal(err) 299 } 300 op.PlanOutBackend = &plans.Backend{ 301 // Just a placeholder so that we can generate a valid plan file. 302 Type: "local", 303 Config: cfgRaw, 304 } 305 run, err := b.Operation(context.Background(), op) 306 if err != nil { 307 t.Fatalf("bad: %s", err) 308 } 309 <-run.Done() 310 if run.Result != backend.OperationSuccess { 311 t.Fatalf("plan operation failed") 312 } 313 if !p.ReadResourceCalled { 314 t.Fatal("ReadResource should be called") 315 } 316 if run.PlanEmpty { 317 t.Fatal("plan should not be empty") 318 } 319 320 expectedOutput := `Terraform used the selected providers to generate the following execution 321 plan. Resource actions are indicated with the following symbols: 322 -/+ destroy and then create replacement 323 324 Terraform will perform the following actions: 325 326 # test_instance.foo is tainted, so must be replaced 327 -/+ resource "test_instance" "foo" { 328 # (1 unchanged attribute hidden) 329 330 # (1 unchanged block hidden) 331 } 332 333 Plan: 1 to add, 0 to change, 1 to destroy.` 334 if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) { 335 t.Fatalf("Unexpected output\ngot\n%s\n\nwant:\n%s", output, expectedOutput) 336 } 337 } 338 339 func TestLocal_planDeposedOnly(t *testing.T) { 340 b := TestLocal(t) 341 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 342 testStateFile(t, b.StatePath, states.BuildState(func(ss *states.SyncState) { 343 ss.SetResourceInstanceDeposed( 344 addrs.Resource{ 345 Mode: addrs.ManagedResourceMode, 346 Type: "test_instance", 347 Name: "foo", 348 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 349 states.DeposedKey("00000000"), 350 &states.ResourceInstanceObjectSrc{ 351 Status: states.ObjectReady, 352 AttrsJSON: []byte(`{ 353 "ami": "bar", 354 "network_interface": [{ 355 "device_index": 0, 356 "description": "Main network interface" 357 }] 358 }`), 359 }, 360 addrs.AbsProviderConfig{ 361 Provider: addrs.NewDefaultProvider("test"), 362 Module: addrs.RootModule, 363 }, 364 ) 365 })) 366 outDir := t.TempDir() 367 planPath := filepath.Join(outDir, "plan.tfplan") 368 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 369 defer configCleanup() 370 op.PlanRefresh = true 371 op.PlanOutPath = planPath 372 cfg := cty.ObjectVal(map[string]cty.Value{ 373 "path": cty.StringVal(b.StatePath), 374 }) 375 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 376 if err != nil { 377 t.Fatal(err) 378 } 379 op.PlanOutBackend = &plans.Backend{ 380 // Just a placeholder so that we can generate a valid plan file. 381 Type: "local", 382 Config: cfgRaw, 383 } 384 run, err := b.Operation(context.Background(), op) 385 if err != nil { 386 t.Fatalf("bad: %s", err) 387 } 388 <-run.Done() 389 if run.Result != backend.OperationSuccess { 390 t.Fatalf("plan operation failed") 391 } 392 if !p.ReadResourceCalled { 393 t.Fatal("ReadResource should've been called to refresh the deposed object") 394 } 395 if run.PlanEmpty { 396 t.Fatal("plan should not be empty") 397 } 398 399 // The deposed object and the current object are distinct, so our 400 // plan includes separate actions for each of them. This strange situation 401 // is not common: it should arise only if Terraform fails during 402 // a create-before-destroy when the create hasn't completed yet but 403 // in a severe way that prevents the previous object from being restored 404 // as "current". 405 // 406 // However, that situation was more common in some earlier Terraform 407 // versions where deposed objects were not managed properly, so this 408 // can arise when upgrading from an older version with deposed objects 409 // already in the state. 410 // 411 // This is one of the few cases where we expose the idea of "deposed" in 412 // the UI, including the user-unfriendly "deposed key" (00000000 in this 413 // case) just so that users can correlate this with what they might 414 // see in `terraform show` and in the subsequent apply output, because 415 // it's also possible for there to be _multiple_ deposed objects, in the 416 // unlikely event that create_before_destroy _keeps_ crashing across 417 // subsequent runs. 418 expectedOutput := `Terraform used the selected providers to generate the following execution 419 plan. Resource actions are indicated with the following symbols: 420 + create 421 - destroy 422 423 Terraform will perform the following actions: 424 425 # test_instance.foo will be created 426 + resource "test_instance" "foo" { 427 + ami = "bar" 428 429 + network_interface { 430 + description = "Main network interface" 431 + device_index = 0 432 } 433 } 434 435 # test_instance.foo (deposed object 00000000) will be destroyed 436 # (left over from a partially-failed replacement of this instance) 437 - resource "test_instance" "foo" { 438 - ami = "bar" -> null 439 440 - network_interface { 441 - description = "Main network interface" -> null 442 - device_index = 0 -> null 443 } 444 } 445 446 Plan: 1 to add, 0 to change, 1 to destroy.` 447 if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) { 448 t.Fatalf("Unexpected output:\n%s", output) 449 } 450 } 451 452 func TestLocal_planTainted_createBeforeDestroy(t *testing.T) { 453 b := TestLocal(t) 454 455 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 456 testStateFile(t, b.StatePath, testPlanState_tainted()) 457 outDir := t.TempDir() 458 planPath := filepath.Join(outDir, "plan.tfplan") 459 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-cbd") 460 defer configCleanup() 461 op.PlanRefresh = true 462 op.PlanOutPath = planPath 463 cfg := cty.ObjectVal(map[string]cty.Value{ 464 "path": cty.StringVal(b.StatePath), 465 }) 466 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 467 if err != nil { 468 t.Fatal(err) 469 } 470 op.PlanOutBackend = &plans.Backend{ 471 // Just a placeholder so that we can generate a valid plan file. 472 Type: "local", 473 Config: cfgRaw, 474 } 475 run, err := b.Operation(context.Background(), op) 476 if err != nil { 477 t.Fatalf("bad: %s", err) 478 } 479 <-run.Done() 480 if run.Result != backend.OperationSuccess { 481 t.Fatalf("plan operation failed") 482 } 483 if !p.ReadResourceCalled { 484 t.Fatal("ReadResource should be called") 485 } 486 if run.PlanEmpty { 487 t.Fatal("plan should not be empty") 488 } 489 490 expectedOutput := `Terraform used the selected providers to generate the following execution 491 plan. Resource actions are indicated with the following symbols: 492 +/- create replacement and then destroy 493 494 Terraform will perform the following actions: 495 496 # test_instance.foo is tainted, so must be replaced 497 +/- resource "test_instance" "foo" { 498 # (1 unchanged attribute hidden) 499 500 # (1 unchanged block hidden) 501 } 502 503 Plan: 1 to add, 0 to change, 1 to destroy.` 504 if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) { 505 t.Fatalf("Unexpected output:\n%s", output) 506 } 507 } 508 509 func TestLocal_planRefreshFalse(t *testing.T) { 510 b := TestLocal(t) 511 512 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 513 testStateFile(t, b.StatePath, testPlanState()) 514 515 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 516 defer configCleanup() 517 518 run, err := b.Operation(context.Background(), op) 519 if err != nil { 520 t.Fatalf("bad: %s", err) 521 } 522 <-run.Done() 523 if run.Result != backend.OperationSuccess { 524 t.Fatalf("plan operation failed") 525 } 526 527 if p.ReadResourceCalled { 528 t.Fatal("ReadResource should not be called") 529 } 530 531 if !run.PlanEmpty { 532 t.Fatal("plan should be empty") 533 } 534 535 if errOutput := done(t).Stderr(); errOutput != "" { 536 t.Fatalf("unexpected error output:\n%s", errOutput) 537 } 538 } 539 540 func TestLocal_planDestroy(t *testing.T) { 541 b := TestLocal(t) 542 543 TestLocalProvider(t, b, "test", planFixtureSchema()) 544 testStateFile(t, b.StatePath, testPlanState()) 545 546 outDir := t.TempDir() 547 planPath := filepath.Join(outDir, "plan.tfplan") 548 549 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 550 defer configCleanup() 551 op.PlanMode = plans.DestroyMode 552 op.PlanRefresh = true 553 op.PlanOutPath = planPath 554 cfg := cty.ObjectVal(map[string]cty.Value{ 555 "path": cty.StringVal(b.StatePath), 556 }) 557 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 558 if err != nil { 559 t.Fatal(err) 560 } 561 op.PlanOutBackend = &plans.Backend{ 562 // Just a placeholder so that we can generate a valid plan file. 563 Type: "local", 564 Config: cfgRaw, 565 } 566 567 run, err := b.Operation(context.Background(), op) 568 if err != nil { 569 t.Fatalf("bad: %s", err) 570 } 571 <-run.Done() 572 if run.Result != backend.OperationSuccess { 573 t.Fatalf("plan operation failed") 574 } 575 576 if run.PlanEmpty { 577 t.Fatal("plan should not be empty") 578 } 579 580 plan := testReadPlan(t, planPath) 581 for _, r := range plan.Changes.Resources { 582 if r.Action.String() != "Delete" { 583 t.Fatalf("bad: %#v", r.Action.String()) 584 } 585 } 586 587 if errOutput := done(t).Stderr(); errOutput != "" { 588 t.Fatalf("unexpected error output:\n%s", errOutput) 589 } 590 } 591 592 func TestLocal_planDestroy_withDataSources(t *testing.T) { 593 b := TestLocal(t) 594 595 TestLocalProvider(t, b, "test", planFixtureSchema()) 596 testStateFile(t, b.StatePath, testPlanState_withDataSource()) 597 598 outDir := t.TempDir() 599 planPath := filepath.Join(outDir, "plan.tfplan") 600 601 op, configCleanup, done := testOperationPlan(t, "./testdata/destroy-with-ds") 602 defer configCleanup() 603 op.PlanMode = plans.DestroyMode 604 op.PlanRefresh = true 605 op.PlanOutPath = planPath 606 cfg := cty.ObjectVal(map[string]cty.Value{ 607 "path": cty.StringVal(b.StatePath), 608 }) 609 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 610 if err != nil { 611 t.Fatal(err) 612 } 613 op.PlanOutBackend = &plans.Backend{ 614 // Just a placeholder so that we can generate a valid plan file. 615 Type: "local", 616 Config: cfgRaw, 617 } 618 619 run, err := b.Operation(context.Background(), op) 620 if err != nil { 621 t.Fatalf("bad: %s", err) 622 } 623 <-run.Done() 624 if run.Result != backend.OperationSuccess { 625 t.Fatalf("plan operation failed") 626 } 627 628 if run.PlanEmpty { 629 t.Fatal("plan should not be empty") 630 } 631 632 // Data source should still exist in the the plan file 633 plan := testReadPlan(t, planPath) 634 if len(plan.Changes.Resources) != 2 { 635 t.Fatalf("Expected exactly 1 resource for destruction, %d given: %q", 636 len(plan.Changes.Resources), getAddrs(plan.Changes.Resources)) 637 } 638 639 // Data source should not be rendered in the output 640 expectedOutput := `Terraform will perform the following actions: 641 642 # test_instance.foo[0] will be destroyed 643 - resource "test_instance" "foo" { 644 - ami = "bar" -> null 645 646 - network_interface { 647 - description = "Main network interface" -> null 648 - device_index = 0 -> null 649 } 650 } 651 652 Plan: 0 to add, 0 to change, 1 to destroy.` 653 654 if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) { 655 t.Fatalf("Unexpected output:\n%s", output) 656 } 657 } 658 659 func getAddrs(resources []*plans.ResourceInstanceChangeSrc) []string { 660 addrs := make([]string, len(resources)) 661 for i, r := range resources { 662 addrs[i] = r.Addr.String() 663 } 664 return addrs 665 } 666 667 func TestLocal_planOutPathNoChange(t *testing.T) { 668 b := TestLocal(t) 669 TestLocalProvider(t, b, "test", planFixtureSchema()) 670 testStateFile(t, b.StatePath, testPlanState()) 671 672 outDir := t.TempDir() 673 planPath := filepath.Join(outDir, "plan.tfplan") 674 675 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 676 defer configCleanup() 677 op.PlanOutPath = planPath 678 cfg := cty.ObjectVal(map[string]cty.Value{ 679 "path": cty.StringVal(b.StatePath), 680 }) 681 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 682 if err != nil { 683 t.Fatal(err) 684 } 685 op.PlanOutBackend = &plans.Backend{ 686 // Just a placeholder so that we can generate a valid plan file. 687 Type: "local", 688 Config: cfgRaw, 689 } 690 op.PlanRefresh = true 691 692 run, err := b.Operation(context.Background(), op) 693 if err != nil { 694 t.Fatalf("bad: %s", err) 695 } 696 <-run.Done() 697 if run.Result != backend.OperationSuccess { 698 t.Fatalf("plan operation failed") 699 } 700 701 plan := testReadPlan(t, planPath) 702 703 if !plan.Changes.Empty() { 704 t.Fatalf("expected empty plan to be written") 705 } 706 707 if errOutput := done(t).Stderr(); errOutput != "" { 708 t.Fatalf("unexpected error output:\n%s", errOutput) 709 } 710 } 711 712 func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { 713 t.Helper() 714 715 _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) 716 717 streams, done := terminal.StreamsForTesting(t) 718 view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) 719 720 // Many of our tests use an overridden "test" provider that's just in-memory 721 // inside the test process, not a separate plugin on disk. 722 depLocks := depsfile.NewLocks() 723 depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test")) 724 725 return &backend.Operation{ 726 Type: backend.OperationTypePlan, 727 ConfigDir: configDir, 728 ConfigLoader: configLoader, 729 StateLocker: clistate.NewNoopLocker(), 730 View: view, 731 DependencyLocks: depLocks, 732 }, configCleanup, done 733 } 734 735 // testPlanState is just a common state that we use for testing plan. 736 func testPlanState() *states.State { 737 state := states.NewState() 738 rootModule := state.RootModule() 739 rootModule.SetResourceInstanceCurrent( 740 addrs.Resource{ 741 Mode: addrs.ManagedResourceMode, 742 Type: "test_instance", 743 Name: "foo", 744 }.Instance(addrs.NoKey), 745 &states.ResourceInstanceObjectSrc{ 746 Status: states.ObjectReady, 747 AttrsJSON: []byte(`{ 748 "ami": "bar", 749 "network_interface": [{ 750 "device_index": 0, 751 "description": "Main network interface" 752 }] 753 }`), 754 }, 755 addrs.AbsProviderConfig{ 756 Provider: addrs.NewDefaultProvider("test"), 757 Module: addrs.RootModule, 758 }, 759 ) 760 return state 761 } 762 763 func testPlanState_withDataSource() *states.State { 764 state := states.NewState() 765 rootModule := state.RootModule() 766 rootModule.SetResourceInstanceCurrent( 767 addrs.Resource{ 768 Mode: addrs.ManagedResourceMode, 769 Type: "test_instance", 770 Name: "foo", 771 }.Instance(addrs.IntKey(0)), 772 &states.ResourceInstanceObjectSrc{ 773 Status: states.ObjectReady, 774 AttrsJSON: []byte(`{ 775 "ami": "bar", 776 "network_interface": [{ 777 "device_index": 0, 778 "description": "Main network interface" 779 }] 780 }`), 781 }, 782 addrs.AbsProviderConfig{ 783 Provider: addrs.NewDefaultProvider("test"), 784 Module: addrs.RootModule, 785 }, 786 ) 787 rootModule.SetResourceInstanceCurrent( 788 addrs.Resource{ 789 Mode: addrs.DataResourceMode, 790 Type: "test_ds", 791 Name: "bar", 792 }.Instance(addrs.IntKey(0)), 793 &states.ResourceInstanceObjectSrc{ 794 Status: states.ObjectReady, 795 AttrsJSON: []byte(`{ 796 "filter": "foo" 797 }`), 798 }, 799 addrs.AbsProviderConfig{ 800 Provider: addrs.NewDefaultProvider("test"), 801 Module: addrs.RootModule, 802 }, 803 ) 804 return state 805 } 806 807 func testPlanState_tainted() *states.State { 808 state := states.NewState() 809 rootModule := state.RootModule() 810 rootModule.SetResourceInstanceCurrent( 811 addrs.Resource{ 812 Mode: addrs.ManagedResourceMode, 813 Type: "test_instance", 814 Name: "foo", 815 }.Instance(addrs.NoKey), 816 &states.ResourceInstanceObjectSrc{ 817 Status: states.ObjectTainted, 818 AttrsJSON: []byte(`{ 819 "ami": "bar", 820 "network_interface": [{ 821 "device_index": 0, 822 "description": "Main network interface" 823 }] 824 }`), 825 }, 826 addrs.AbsProviderConfig{ 827 Provider: addrs.NewDefaultProvider("test"), 828 Module: addrs.RootModule, 829 }, 830 ) 831 return state 832 } 833 834 func testReadPlan(t *testing.T, path string) *plans.Plan { 835 t.Helper() 836 837 p, err := planfile.Open(path) 838 if err != nil { 839 t.Fatalf("err: %s", err) 840 } 841 defer p.Close() 842 843 plan, err := p.ReadPlan() 844 if err != nil { 845 t.Fatalf("err: %s", err) 846 } 847 848 return plan 849 } 850 851 // planFixtureSchema returns a schema suitable for processing the 852 // configuration in testdata/plan . This schema should be 853 // assigned to a mock provider named "test". 854 func planFixtureSchema() *terraform.ProviderSchema { 855 return &terraform.ProviderSchema{ 856 ResourceTypes: map[string]*configschema.Block{ 857 "test_instance": { 858 Attributes: map[string]*configschema.Attribute{ 859 "ami": {Type: cty.String, Optional: true}, 860 }, 861 BlockTypes: map[string]*configschema.NestedBlock{ 862 "network_interface": { 863 Nesting: configschema.NestingList, 864 Block: configschema.Block{ 865 Attributes: map[string]*configschema.Attribute{ 866 "device_index": {Type: cty.Number, Optional: true}, 867 "description": {Type: cty.String, Optional: true}, 868 }, 869 }, 870 }, 871 }, 872 }, 873 }, 874 DataSources: map[string]*configschema.Block{ 875 "test_ds": { 876 Attributes: map[string]*configschema.Attribute{ 877 "filter": {Type: cty.String, Required: true}, 878 }, 879 }, 880 }, 881 } 882 }