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