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