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