github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/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/iaas-resource-provision/iaas-rpc/internal/addrs" 11 "github.com/iaas-resource-provision/iaas-rpc/internal/backend" 12 "github.com/iaas-resource-provision/iaas-rpc/internal/command/arguments" 13 "github.com/iaas-resource-provision/iaas-rpc/internal/command/clistate" 14 "github.com/iaas-resource-provision/iaas-rpc/internal/command/views" 15 "github.com/iaas-resource-provision/iaas-rpc/internal/configs/configschema" 16 "github.com/iaas-resource-provision/iaas-rpc/internal/initwd" 17 "github.com/iaas-resource-provision/iaas-rpc/internal/plans" 18 "github.com/iaas-resource-provision/iaas-rpc/internal/plans/planfile" 19 "github.com/iaas-resource-provision/iaas-rpc/internal/states" 20 "github.com/iaas-resource-provision/iaas-rpc/internal/terminal" 21 "github.com/iaas-resource-provision/iaas-rpc/internal/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 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 123 defer configCleanup() 124 op.PlanRefresh = true 125 126 // we coerce a failure in Context() by omitting the provider schema 127 run, err := b.Operation(context.Background(), op) 128 if err != nil { 129 t.Fatalf("bad: %s", err) 130 } 131 <-run.Done() 132 if run.Result != backend.OperationFailure { 133 t.Fatalf("plan operation succeeded") 134 } 135 136 // the backend should be unlocked after a run 137 assertBackendStateUnlocked(t, b) 138 139 if got, want := done(t).Stderr(), "Error: Could not load plugin"; !strings.Contains(got, want) { 140 t.Fatalf("unexpected error output:\n%s\nwant: %s", got, want) 141 } 142 } 143 144 func TestLocal_planOutputsChanged(t *testing.T) { 145 b, cleanup := TestLocal(t) 146 defer cleanup() 147 testStateFile(t, b.StatePath, states.BuildState(func(ss *states.SyncState) { 148 ss.SetOutputValue(addrs.AbsOutputValue{ 149 Module: addrs.RootModuleInstance, 150 OutputValue: addrs.OutputValue{Name: "changed"}, 151 }, cty.StringVal("before"), false) 152 ss.SetOutputValue(addrs.AbsOutputValue{ 153 Module: addrs.RootModuleInstance, 154 OutputValue: addrs.OutputValue{Name: "sensitive_before"}, 155 }, cty.StringVal("before"), true) 156 ss.SetOutputValue(addrs.AbsOutputValue{ 157 Module: addrs.RootModuleInstance, 158 OutputValue: addrs.OutputValue{Name: "sensitive_after"}, 159 }, cty.StringVal("before"), false) 160 ss.SetOutputValue(addrs.AbsOutputValue{ 161 Module: addrs.RootModuleInstance, 162 OutputValue: addrs.OutputValue{Name: "removed"}, // not present in the config fixture 163 }, cty.StringVal("before"), false) 164 ss.SetOutputValue(addrs.AbsOutputValue{ 165 Module: addrs.RootModuleInstance, 166 OutputValue: addrs.OutputValue{Name: "unchanged"}, 167 }, cty.StringVal("before"), false) 168 // NOTE: This isn't currently testing the situation where the new 169 // value of an output is unknown, because to do that requires there to 170 // be at least one managed resource Create action in the plan and that 171 // would defeat the point of this test, which is to ensure that a 172 // plan containing only output changes is considered "non-empty". 173 // For now we're not too worried about testing the "new value is 174 // unknown" situation because that's already common for printing out 175 // resource changes and we already have many tests for that. 176 })) 177 outDir := testTempDir(t) 178 defer os.RemoveAll(outDir) 179 planPath := filepath.Join(outDir, "plan.tfplan") 180 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-outputs-changed") 181 defer configCleanup() 182 op.PlanRefresh = true 183 op.PlanOutPath = planPath 184 cfg := cty.ObjectVal(map[string]cty.Value{ 185 "path": cty.StringVal(b.StatePath), 186 }) 187 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 188 if err != nil { 189 t.Fatal(err) 190 } 191 op.PlanOutBackend = &plans.Backend{ 192 // Just a placeholder so that we can generate a valid plan file. 193 Type: "local", 194 Config: cfgRaw, 195 } 196 run, err := b.Operation(context.Background(), op) 197 if err != nil { 198 t.Fatalf("bad: %s", err) 199 } 200 <-run.Done() 201 if run.Result != backend.OperationSuccess { 202 t.Fatalf("plan operation failed") 203 } 204 if run.PlanEmpty { 205 t.Error("plan should not be empty") 206 } 207 208 expectedOutput := strings.TrimSpace(` 209 Changes to Outputs: 210 + added = "after" 211 ~ changed = "before" -> "after" 212 - removed = "before" -> null 213 ~ sensitive_after = (sensitive value) 214 ~ sensitive_before = (sensitive value) 215 216 You can apply this plan to save these new output values to the Terraform 217 state, without changing any real infrastructure. 218 `) 219 220 if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) { 221 t.Errorf("Unexpected output:\n%s\n\nwant output containing:\n%s", output, expectedOutput) 222 } 223 } 224 225 // Module outputs should not cause the plan to be rendered 226 func TestLocal_planModuleOutputsChanged(t *testing.T) { 227 b, cleanup := TestLocal(t) 228 defer cleanup() 229 testStateFile(t, b.StatePath, states.BuildState(func(ss *states.SyncState) { 230 ss.SetOutputValue(addrs.AbsOutputValue{ 231 Module: addrs.RootModuleInstance.Child("mod", addrs.NoKey), 232 OutputValue: addrs.OutputValue{Name: "changed"}, 233 }, cty.StringVal("before"), false) 234 })) 235 outDir := testTempDir(t) 236 defer os.RemoveAll(outDir) 237 planPath := filepath.Join(outDir, "plan.tfplan") 238 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-module-outputs-changed") 239 defer configCleanup() 240 op.PlanRefresh = true 241 op.PlanOutPath = planPath 242 cfg := cty.ObjectVal(map[string]cty.Value{ 243 "path": cty.StringVal(b.StatePath), 244 }) 245 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 246 if err != nil { 247 t.Fatal(err) 248 } 249 op.PlanOutBackend = &plans.Backend{ 250 Type: "local", 251 Config: cfgRaw, 252 } 253 run, err := b.Operation(context.Background(), op) 254 if err != nil { 255 t.Fatalf("bad: %s", err) 256 } 257 <-run.Done() 258 if run.Result != backend.OperationSuccess { 259 t.Fatalf("plan operation failed") 260 } 261 if !run.PlanEmpty { 262 t.Fatal("plan should be empty") 263 } 264 265 expectedOutput := strings.TrimSpace(` 266 No changes. Your infrastructure matches the configuration. 267 `) 268 if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) { 269 t.Fatalf("Unexpected output:\n%s\n\nwant output containing:\n%s", output, expectedOutput) 270 } 271 } 272 273 func TestLocal_planTainted(t *testing.T) { 274 b, cleanup := TestLocal(t) 275 defer cleanup() 276 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 277 testStateFile(t, b.StatePath, testPlanState_tainted()) 278 outDir := testTempDir(t) 279 defer os.RemoveAll(outDir) 280 planPath := filepath.Join(outDir, "plan.tfplan") 281 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 282 defer configCleanup() 283 op.PlanRefresh = true 284 op.PlanOutPath = planPath 285 cfg := cty.ObjectVal(map[string]cty.Value{ 286 "path": cty.StringVal(b.StatePath), 287 }) 288 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 289 if err != nil { 290 t.Fatal(err) 291 } 292 op.PlanOutBackend = &plans.Backend{ 293 // Just a placeholder so that we can generate a valid plan file. 294 Type: "local", 295 Config: cfgRaw, 296 } 297 run, err := b.Operation(context.Background(), op) 298 if err != nil { 299 t.Fatalf("bad: %s", err) 300 } 301 <-run.Done() 302 if run.Result != backend.OperationSuccess { 303 t.Fatalf("plan operation failed") 304 } 305 if !p.ReadResourceCalled { 306 t.Fatal("ReadResource should be called") 307 } 308 if run.PlanEmpty { 309 t.Fatal("plan should not be empty") 310 } 311 312 expectedOutput := `Terraform used the selected providers to generate the following execution 313 plan. Resource actions are indicated with the following symbols: 314 -/+ destroy and then create replacement 315 316 Terraform will perform the following actions: 317 318 # test_instance.foo is tainted, so must be replaced 319 -/+ resource "test_instance" "foo" { 320 # (1 unchanged attribute hidden) 321 322 # (1 unchanged block hidden) 323 } 324 325 Plan: 1 to add, 0 to change, 1 to destroy.` 326 if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) { 327 t.Fatalf("Unexpected output\ngot\n%s\n\nwant:\n%s", output, expectedOutput) 328 } 329 } 330 331 func TestLocal_planDeposedOnly(t *testing.T) { 332 b, cleanup := TestLocal(t) 333 defer cleanup() 334 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 335 testStateFile(t, b.StatePath, states.BuildState(func(ss *states.SyncState) { 336 ss.SetResourceInstanceDeposed( 337 addrs.Resource{ 338 Mode: addrs.ManagedResourceMode, 339 Type: "test_instance", 340 Name: "foo", 341 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 342 states.DeposedKey("00000000"), 343 &states.ResourceInstanceObjectSrc{ 344 Status: states.ObjectReady, 345 AttrsJSON: []byte(`{ 346 "ami": "bar", 347 "network_interface": [{ 348 "device_index": 0, 349 "description": "Main network interface" 350 }] 351 }`), 352 }, 353 addrs.AbsProviderConfig{ 354 Provider: addrs.NewDefaultProvider("test"), 355 Module: addrs.RootModule, 356 }, 357 ) 358 })) 359 outDir := testTempDir(t) 360 defer os.RemoveAll(outDir) 361 planPath := filepath.Join(outDir, "plan.tfplan") 362 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 363 defer configCleanup() 364 op.PlanRefresh = true 365 op.PlanOutPath = planPath 366 cfg := cty.ObjectVal(map[string]cty.Value{ 367 "path": cty.StringVal(b.StatePath), 368 }) 369 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 370 if err != nil { 371 t.Fatal(err) 372 } 373 op.PlanOutBackend = &plans.Backend{ 374 // Just a placeholder so that we can generate a valid plan file. 375 Type: "local", 376 Config: cfgRaw, 377 } 378 run, err := b.Operation(context.Background(), op) 379 if err != nil { 380 t.Fatalf("bad: %s", err) 381 } 382 <-run.Done() 383 if run.Result != backend.OperationSuccess { 384 t.Fatalf("plan operation failed") 385 } 386 if !p.ReadResourceCalled { 387 t.Fatal("ReadResource should've been called to refresh the deposed object") 388 } 389 if run.PlanEmpty { 390 t.Fatal("plan should not be empty") 391 } 392 393 // The deposed object and the current object are distinct, so our 394 // plan includes separate actions for each of them. This strange situation 395 // is not common: it should arise only if Terraform fails during 396 // a create-before-destroy when the create hasn't completed yet but 397 // in a severe way that prevents the previous object from being restored 398 // as "current". 399 // 400 // However, that situation was more common in some earlier Terraform 401 // versions where deposed objects were not managed properly, so this 402 // can arise when upgrading from an older version with deposed objects 403 // already in the state. 404 // 405 // This is one of the few cases where we expose the idea of "deposed" in 406 // the UI, including the user-unfriendly "deposed key" (00000000 in this 407 // case) just so that users can correlate this with what they might 408 // see in `terraform show` and in the subsequent apply output, because 409 // it's also possible for there to be _multiple_ deposed objects, in the 410 // unlikely event that create_before_destroy _keeps_ crashing across 411 // subsequent runs. 412 expectedOutput := `Terraform used the selected providers to generate the following execution 413 plan. Resource actions are indicated with the following symbols: 414 + create 415 - destroy 416 417 Terraform will perform the following actions: 418 419 # test_instance.foo will be created 420 + resource "test_instance" "foo" { 421 + ami = "bar" 422 423 + network_interface { 424 + description = "Main network interface" 425 + device_index = 0 426 } 427 } 428 429 # test_instance.foo (deposed object 00000000) will be destroyed 430 # (left over from a partially-failed replacement of this instance) 431 - resource "test_instance" "foo" { 432 - ami = "bar" -> null 433 434 - network_interface { 435 - description = "Main network interface" -> null 436 - device_index = 0 -> null 437 } 438 } 439 440 Plan: 1 to add, 0 to change, 1 to destroy.` 441 if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) { 442 t.Fatalf("Unexpected output:\n%s", output) 443 } 444 } 445 446 func TestLocal_planTainted_createBeforeDestroy(t *testing.T) { 447 b, cleanup := TestLocal(t) 448 defer cleanup() 449 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 450 testStateFile(t, b.StatePath, testPlanState_tainted()) 451 outDir := testTempDir(t) 452 defer os.RemoveAll(outDir) 453 planPath := filepath.Join(outDir, "plan.tfplan") 454 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-cbd") 455 defer configCleanup() 456 op.PlanRefresh = true 457 op.PlanOutPath = planPath 458 cfg := cty.ObjectVal(map[string]cty.Value{ 459 "path": cty.StringVal(b.StatePath), 460 }) 461 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 462 if err != nil { 463 t.Fatal(err) 464 } 465 op.PlanOutBackend = &plans.Backend{ 466 // Just a placeholder so that we can generate a valid plan file. 467 Type: "local", 468 Config: cfgRaw, 469 } 470 run, err := b.Operation(context.Background(), op) 471 if err != nil { 472 t.Fatalf("bad: %s", err) 473 } 474 <-run.Done() 475 if run.Result != backend.OperationSuccess { 476 t.Fatalf("plan operation failed") 477 } 478 if !p.ReadResourceCalled { 479 t.Fatal("ReadResource should be called") 480 } 481 if run.PlanEmpty { 482 t.Fatal("plan should not be empty") 483 } 484 485 expectedOutput := `Terraform used the selected providers to generate the following execution 486 plan. Resource actions are indicated with the following symbols: 487 +/- create replacement and then destroy 488 489 Terraform will perform the following actions: 490 491 # test_instance.foo is tainted, so must be replaced 492 +/- resource "test_instance" "foo" { 493 # (1 unchanged attribute hidden) 494 495 # (1 unchanged block hidden) 496 } 497 498 Plan: 1 to add, 0 to change, 1 to destroy.` 499 if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) { 500 t.Fatalf("Unexpected output:\n%s", output) 501 } 502 } 503 504 func TestLocal_planRefreshFalse(t *testing.T) { 505 b, cleanup := TestLocal(t) 506 defer cleanup() 507 508 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 509 testStateFile(t, b.StatePath, testPlanState()) 510 511 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 512 defer configCleanup() 513 514 run, err := b.Operation(context.Background(), op) 515 if err != nil { 516 t.Fatalf("bad: %s", err) 517 } 518 <-run.Done() 519 if run.Result != backend.OperationSuccess { 520 t.Fatalf("plan operation failed") 521 } 522 523 if p.ReadResourceCalled { 524 t.Fatal("ReadResource should not be called") 525 } 526 527 if !run.PlanEmpty { 528 t.Fatal("plan should be empty") 529 } 530 531 if errOutput := done(t).Stderr(); errOutput != "" { 532 t.Fatalf("unexpected error output:\n%s", errOutput) 533 } 534 } 535 536 func TestLocal_planDestroy(t *testing.T) { 537 b, cleanup := TestLocal(t) 538 defer cleanup() 539 540 TestLocalProvider(t, b, "test", planFixtureSchema()) 541 testStateFile(t, b.StatePath, testPlanState()) 542 543 outDir := testTempDir(t) 544 defer os.RemoveAll(outDir) 545 planPath := filepath.Join(outDir, "plan.tfplan") 546 547 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 548 defer configCleanup() 549 op.PlanMode = plans.DestroyMode 550 op.PlanRefresh = true 551 op.PlanOutPath = planPath 552 cfg := cty.ObjectVal(map[string]cty.Value{ 553 "path": cty.StringVal(b.StatePath), 554 }) 555 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 556 if err != nil { 557 t.Fatal(err) 558 } 559 op.PlanOutBackend = &plans.Backend{ 560 // Just a placeholder so that we can generate a valid plan file. 561 Type: "local", 562 Config: cfgRaw, 563 } 564 565 run, err := b.Operation(context.Background(), op) 566 if err != nil { 567 t.Fatalf("bad: %s", err) 568 } 569 <-run.Done() 570 if run.Result != backend.OperationSuccess { 571 t.Fatalf("plan operation failed") 572 } 573 574 if run.PlanEmpty { 575 t.Fatal("plan should not be empty") 576 } 577 578 plan := testReadPlan(t, planPath) 579 for _, r := range plan.Changes.Resources { 580 if r.Action.String() != "Delete" { 581 t.Fatalf("bad: %#v", r.Action.String()) 582 } 583 } 584 585 if errOutput := done(t).Stderr(); errOutput != "" { 586 t.Fatalf("unexpected error output:\n%s", errOutput) 587 } 588 } 589 590 func TestLocal_planDestroy_withDataSources(t *testing.T) { 591 b, cleanup := TestLocal(t) 592 defer cleanup() 593 594 TestLocalProvider(t, b, "test", planFixtureSchema()) 595 testStateFile(t, b.StatePath, testPlanState_withDataSource()) 596 597 outDir := testTempDir(t) 598 defer os.RemoveAll(outDir) 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, cleanup := TestLocal(t) 669 defer cleanup() 670 TestLocalProvider(t, b, "test", planFixtureSchema()) 671 testStateFile(t, b.StatePath, testPlanState()) 672 673 outDir := testTempDir(t) 674 defer os.RemoveAll(outDir) 675 planPath := filepath.Join(outDir, "plan.tfplan") 676 677 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 678 defer configCleanup() 679 op.PlanOutPath = planPath 680 cfg := cty.ObjectVal(map[string]cty.Value{ 681 "path": cty.StringVal(b.StatePath), 682 }) 683 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 684 if err != nil { 685 t.Fatal(err) 686 } 687 op.PlanOutBackend = &plans.Backend{ 688 // Just a placeholder so that we can generate a valid plan file. 689 Type: "local", 690 Config: cfgRaw, 691 } 692 op.PlanRefresh = true 693 694 run, err := b.Operation(context.Background(), op) 695 if err != nil { 696 t.Fatalf("bad: %s", err) 697 } 698 <-run.Done() 699 if run.Result != backend.OperationSuccess { 700 t.Fatalf("plan operation failed") 701 } 702 703 plan := testReadPlan(t, planPath) 704 705 if !plan.Changes.Empty() { 706 t.Fatalf("expected empty plan to be written") 707 } 708 709 if errOutput := done(t).Stderr(); errOutput != "" { 710 t.Fatalf("unexpected error output:\n%s", errOutput) 711 } 712 } 713 714 func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { 715 t.Helper() 716 717 _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) 718 719 streams, done := terminal.StreamsForTesting(t) 720 view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) 721 722 return &backend.Operation{ 723 Type: backend.OperationTypePlan, 724 ConfigDir: configDir, 725 ConfigLoader: configLoader, 726 StateLocker: clistate.NewNoopLocker(), 727 View: view, 728 }, configCleanup, done 729 } 730 731 // testPlanState is just a common state that we use for testing plan. 732 func testPlanState() *states.State { 733 state := states.NewState() 734 rootModule := state.RootModule() 735 rootModule.SetResourceInstanceCurrent( 736 addrs.Resource{ 737 Mode: addrs.ManagedResourceMode, 738 Type: "test_instance", 739 Name: "foo", 740 }.Instance(addrs.IntKey(0)), 741 &states.ResourceInstanceObjectSrc{ 742 Status: states.ObjectReady, 743 AttrsJSON: []byte(`{ 744 "ami": "bar", 745 "network_interface": [{ 746 "device_index": 0, 747 "description": "Main network interface" 748 }] 749 }`), 750 }, 751 addrs.AbsProviderConfig{ 752 Provider: addrs.NewDefaultProvider("test"), 753 Module: addrs.RootModule, 754 }, 755 ) 756 return state 757 } 758 759 func testPlanState_withDataSource() *states.State { 760 state := states.NewState() 761 rootModule := state.RootModule() 762 rootModule.SetResourceInstanceCurrent( 763 addrs.Resource{ 764 Mode: addrs.ManagedResourceMode, 765 Type: "test_instance", 766 Name: "foo", 767 }.Instance(addrs.IntKey(0)), 768 &states.ResourceInstanceObjectSrc{ 769 Status: states.ObjectReady, 770 AttrsJSON: []byte(`{ 771 "ami": "bar", 772 "network_interface": [{ 773 "device_index": 0, 774 "description": "Main network interface" 775 }] 776 }`), 777 }, 778 addrs.AbsProviderConfig{ 779 Provider: addrs.NewDefaultProvider("test"), 780 Module: addrs.RootModule, 781 }, 782 ) 783 rootModule.SetResourceInstanceCurrent( 784 addrs.Resource{ 785 Mode: addrs.DataResourceMode, 786 Type: "test_ds", 787 Name: "bar", 788 }.Instance(addrs.IntKey(0)), 789 &states.ResourceInstanceObjectSrc{ 790 Status: states.ObjectReady, 791 AttrsJSON: []byte(`{ 792 "filter": "foo" 793 }`), 794 }, 795 addrs.AbsProviderConfig{ 796 Provider: addrs.NewDefaultProvider("test"), 797 Module: addrs.RootModule, 798 }, 799 ) 800 return state 801 } 802 803 func testPlanState_tainted() *states.State { 804 state := states.NewState() 805 rootModule := state.RootModule() 806 rootModule.SetResourceInstanceCurrent( 807 addrs.Resource{ 808 Mode: addrs.ManagedResourceMode, 809 Type: "test_instance", 810 Name: "foo", 811 }.Instance(addrs.NoKey), 812 &states.ResourceInstanceObjectSrc{ 813 Status: states.ObjectTainted, 814 AttrsJSON: []byte(`{ 815 "ami": "bar", 816 "network_interface": [{ 817 "device_index": 0, 818 "description": "Main network interface" 819 }] 820 }`), 821 }, 822 addrs.AbsProviderConfig{ 823 Provider: addrs.NewDefaultProvider("test"), 824 Module: addrs.RootModule, 825 }, 826 ) 827 return state 828 } 829 830 func testReadPlan(t *testing.T, path string) *plans.Plan { 831 t.Helper() 832 833 p, err := planfile.Open(path) 834 if err != nil { 835 t.Fatalf("err: %s", err) 836 } 837 defer p.Close() 838 839 plan, err := p.ReadPlan() 840 if err != nil { 841 t.Fatalf("err: %s", err) 842 } 843 844 return plan 845 } 846 847 // planFixtureSchema returns a schema suitable for processing the 848 // configuration in testdata/plan . This schema should be 849 // assigned to a mock provider named "test". 850 func planFixtureSchema() *terraform.ProviderSchema { 851 return &terraform.ProviderSchema{ 852 ResourceTypes: map[string]*configschema.Block{ 853 "test_instance": { 854 Attributes: map[string]*configschema.Attribute{ 855 "ami": {Type: cty.String, Optional: true}, 856 }, 857 BlockTypes: map[string]*configschema.NestedBlock{ 858 "network_interface": { 859 Nesting: configschema.NestingList, 860 Block: configschema.Block{ 861 Attributes: map[string]*configschema.Attribute{ 862 "device_index": {Type: cty.Number, Optional: true}, 863 "description": {Type: cty.String, Optional: true}, 864 }, 865 }, 866 }, 867 }, 868 }, 869 }, 870 DataSources: map[string]*configschema.Block{ 871 "test_ds": { 872 Attributes: map[string]*configschema.Attribute{ 873 "filter": {Type: cty.String, Required: true}, 874 }, 875 }, 876 }, 877 } 878 }