github.com/opentofu/opentofu@v1.7.1/internal/tofu/context_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 tofu 7 8 import ( 9 "bufio" 10 "bytes" 11 "fmt" 12 "path/filepath" 13 "sort" 14 "strings" 15 "testing" 16 "time" 17 18 "github.com/google/go-cmp/cmp" 19 "github.com/google/go-cmp/cmp/cmpopts" 20 "github.com/hashicorp/go-version" 21 "github.com/opentofu/opentofu/internal/configs" 22 "github.com/opentofu/opentofu/internal/configs/configload" 23 "github.com/opentofu/opentofu/internal/configs/configschema" 24 "github.com/opentofu/opentofu/internal/configs/hcl2shim" 25 "github.com/opentofu/opentofu/internal/encryption" 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/provisioners" 30 "github.com/opentofu/opentofu/internal/states" 31 "github.com/opentofu/opentofu/internal/states/statefile" 32 "github.com/opentofu/opentofu/internal/tfdiags" 33 tfversion "github.com/opentofu/opentofu/version" 34 "github.com/zclconf/go-cty/cty" 35 ) 36 37 var ( 38 equateEmpty = cmpopts.EquateEmpty() 39 typeComparer = cmp.Comparer(cty.Type.Equals) 40 valueComparer = cmp.Comparer(cty.Value.RawEquals) 41 valueTrans = cmp.Transformer("hcl2shim", hcl2shim.ConfigValueFromHCL2) 42 ) 43 44 func TestNewContextRequiredVersion(t *testing.T) { 45 cases := []struct { 46 Name string 47 Version string 48 Value string 49 Err bool 50 }{ 51 { 52 "no requirement", 53 "0.1.0", 54 "", 55 false, 56 }, 57 58 { 59 "doesn't match", 60 "0.1.0", 61 "> 0.6.0", 62 true, 63 }, 64 65 { 66 "matches", 67 "0.7.0", 68 "> 0.6.0", 69 false, 70 }, 71 72 { 73 "prerelease doesn't match with inequality", 74 "0.8.0", 75 "> 0.7.0-beta", 76 true, 77 }, 78 79 { 80 "prerelease doesn't match with equality", 81 "0.7.0", 82 "0.7.0-beta", 83 true, 84 }, 85 } 86 87 for i, tc := range cases { 88 t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { 89 // Reset the version for the tests 90 old := tfversion.SemVer 91 tfversion.SemVer = version.Must(version.NewVersion(tc.Version)) 92 defer func() { tfversion.SemVer = old }() 93 94 mod := testModule(t, "context-required-version") 95 if tc.Value != "" { 96 constraint, err := version.NewConstraint(tc.Value) 97 if err != nil { 98 t.Fatalf("can't parse %q as version constraint", tc.Value) 99 } 100 mod.Module.CoreVersionConstraints = append(mod.Module.CoreVersionConstraints, configs.VersionConstraint{ 101 Required: constraint, 102 }) 103 } 104 c, diags := NewContext(&ContextOpts{}) 105 if diags.HasErrors() { 106 t.Fatalf("unexpected NewContext errors: %s", diags.Err()) 107 } 108 109 diags = c.Validate(mod) 110 if diags.HasErrors() != tc.Err { 111 t.Fatalf("err: %s", diags.Err()) 112 } 113 }) 114 } 115 } 116 117 func TestNewContextRequiredVersion_child(t *testing.T) { 118 mod := testModuleInline(t, map[string]string{ 119 "main.tf": ` 120 module "child" { 121 source = "./child" 122 } 123 `, 124 "child/main.tf": ` 125 terraform {} 126 `, 127 }) 128 129 cases := map[string]struct { 130 Version string 131 Constraint string 132 Err bool 133 }{ 134 "matches": { 135 "0.5.0", 136 ">= 0.5.0", 137 false, 138 }, 139 "doesn't match": { 140 "0.4.0", 141 ">= 0.5.0", 142 true, 143 }, 144 } 145 146 for name, tc := range cases { 147 t.Run(name, func(t *testing.T) { 148 // Reset the version for the tests 149 old := tfversion.SemVer 150 tfversion.SemVer = version.Must(version.NewVersion(tc.Version)) 151 defer func() { tfversion.SemVer = old }() 152 153 if tc.Constraint != "" { 154 constraint, err := version.NewConstraint(tc.Constraint) 155 if err != nil { 156 t.Fatalf("can't parse %q as version constraint", tc.Constraint) 157 } 158 child := mod.Children["child"] 159 child.Module.CoreVersionConstraints = append(child.Module.CoreVersionConstraints, configs.VersionConstraint{ 160 Required: constraint, 161 }) 162 } 163 c, diags := NewContext(&ContextOpts{}) 164 if diags.HasErrors() { 165 t.Fatalf("unexpected NewContext errors: %s", diags.Err()) 166 } 167 168 diags = c.Validate(mod) 169 if diags.HasErrors() != tc.Err { 170 t.Fatalf("err: %s", diags.Err()) 171 } 172 }) 173 } 174 } 175 176 func TestContext_missingPlugins(t *testing.T) { 177 ctx, diags := NewContext(&ContextOpts{}) 178 assertNoDiagnostics(t, diags) 179 180 configSrc := ` 181 terraform { 182 required_providers { 183 explicit = { 184 source = "example.com/foo/beep" 185 } 186 builtin = { 187 source = "terraform.io/builtin/nonexist" 188 } 189 } 190 } 191 192 resource "implicit_thing" "a" { 193 provisioner "nonexist" { 194 } 195 } 196 197 resource "implicit_thing" "b" { 198 provider = implicit2 199 } 200 ` 201 202 cfg := testModuleInline(t, map[string]string{ 203 "main.tf": configSrc, 204 }) 205 206 // Validate and Plan are the two entry points where we explicitly verify 207 // the available plugins match what the configuration needs. For other 208 // operations we typically fail more deeply in OpenTofu Core, with 209 // potentially-less-helpful error messages, because getting there would 210 // require doing some pretty weird things that aren't common enough to 211 // be worth the complexity to check for them. 212 213 validateDiags := ctx.Validate(cfg) 214 _, planDiags := ctx.Plan(cfg, nil, DefaultPlanOpts) 215 216 tests := map[string]tfdiags.Diagnostics{ 217 "validate": validateDiags, 218 "plan": planDiags, 219 } 220 221 for testName, gotDiags := range tests { 222 t.Run(testName, func(t *testing.T) { 223 var wantDiags tfdiags.Diagnostics 224 wantDiags = wantDiags.Append( 225 tfdiags.Sourceless( 226 tfdiags.Error, 227 "Missing required provider", 228 "This configuration requires built-in provider terraform.io/builtin/nonexist, but that provider isn't available in this OpenTofu version.", 229 ), 230 tfdiags.Sourceless( 231 tfdiags.Error, 232 "Missing required provider", 233 "This configuration requires provider example.com/foo/beep, but that provider isn't available. You may be able to install it automatically by running:\n tofu init", 234 ), 235 tfdiags.Sourceless( 236 tfdiags.Error, 237 "Missing required provider", 238 "This configuration requires provider registry.opentofu.org/hashicorp/implicit, but that provider isn't available. You may be able to install it automatically by running:\n tofu init", 239 ), 240 tfdiags.Sourceless( 241 tfdiags.Error, 242 "Missing required provider", 243 "This configuration requires provider registry.opentofu.org/hashicorp/implicit2, but that provider isn't available. You may be able to install it automatically by running:\n tofu init", 244 ), 245 tfdiags.Sourceless( 246 tfdiags.Error, 247 "Missing required provisioner plugin", 248 `This configuration requires provisioner plugin "nonexist", which isn't available. If you're intending to use an external provisioner plugin, you must install it manually into one of the plugin search directories before running OpenTofu.`, 249 ), 250 ) 251 assertDiagnosticsMatch(t, gotDiags, wantDiags) 252 }) 253 } 254 } 255 256 func testContext2(t *testing.T, opts *ContextOpts) *Context { 257 t.Helper() 258 259 ctx, diags := NewContext(opts) 260 if diags.HasErrors() { 261 t.Fatalf("failed to create test context\n\n%s\n", diags.Err()) 262 } 263 264 ctx.encryption = encryption.Disabled() 265 266 return ctx 267 } 268 269 func testApplyFn(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { 270 resp.NewState = req.PlannedState 271 if req.PlannedState.IsNull() { 272 resp.NewState = cty.NullVal(req.PriorState.Type()) 273 return 274 } 275 276 planned := req.PlannedState.AsValueMap() 277 if planned == nil { 278 planned = map[string]cty.Value{} 279 } 280 281 id, ok := planned["id"] 282 if !ok || id.IsNull() || !id.IsKnown() { 283 planned["id"] = cty.StringVal("foo") 284 } 285 286 // our default schema has a computed "type" attr 287 if ty, ok := planned["type"]; ok && !ty.IsNull() { 288 planned["type"] = cty.StringVal(req.TypeName) 289 } 290 291 if cmp, ok := planned["compute"]; ok && !cmp.IsNull() { 292 computed := cmp.AsString() 293 if val, ok := planned[computed]; ok && !val.IsKnown() { 294 planned[computed] = cty.StringVal("computed_value") 295 } 296 } 297 298 for k, v := range planned { 299 if k == "unknown" { 300 // "unknown" should cause an error 301 continue 302 } 303 304 if !v.IsKnown() { 305 switch k { 306 case "type": 307 planned[k] = cty.StringVal(req.TypeName) 308 default: 309 planned[k] = cty.NullVal(v.Type()) 310 } 311 } 312 } 313 314 resp.NewState = cty.ObjectVal(planned) 315 return 316 } 317 318 func testDiffFn(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 319 var planned map[string]cty.Value 320 321 // this is a destroy plan 322 if req.ProposedNewState.IsNull() { 323 resp.PlannedState = req.ProposedNewState 324 resp.PlannedPrivate = req.PriorPrivate 325 return resp 326 } 327 328 if !req.ProposedNewState.IsNull() { 329 planned = req.ProposedNewState.AsValueMap() 330 } 331 if planned == nil { 332 planned = map[string]cty.Value{} 333 } 334 335 // id is always computed for the tests 336 if id, ok := planned["id"]; ok && id.IsNull() { 337 planned["id"] = cty.UnknownVal(cty.String) 338 } 339 340 // the old tests have require_new replace on every plan 341 if _, ok := planned["require_new"]; ok { 342 resp.RequiresReplace = append(resp.RequiresReplace, cty.Path{cty.GetAttrStep{Name: "require_new"}}) 343 } 344 345 for k := range planned { 346 requiresNewKey := "__" + k + "_requires_new" 347 _, ok := planned[requiresNewKey] 348 if ok { 349 resp.RequiresReplace = append(resp.RequiresReplace, cty.Path{cty.GetAttrStep{Name: requiresNewKey}}) 350 } 351 } 352 353 if v, ok := planned["compute"]; ok && !v.IsNull() { 354 k := v.AsString() 355 unknown := cty.UnknownVal(cty.String) 356 if strings.HasSuffix(k, ".#") { 357 k = k[:len(k)-2] 358 unknown = cty.UnknownVal(cty.List(cty.String)) 359 } 360 planned[k] = unknown 361 } 362 363 if t, ok := planned["type"]; ok && t.IsNull() { 364 planned["type"] = cty.UnknownVal(cty.String) 365 } 366 367 resp.PlannedState = cty.ObjectVal(planned) 368 return 369 } 370 371 func testProvider(prefix string) *MockProvider { 372 p := new(MockProvider) 373 p.GetProviderSchemaResponse = testProviderSchema(prefix) 374 375 return p 376 } 377 378 func testProvisioner() *MockProvisioner { 379 p := new(MockProvisioner) 380 p.GetSchemaResponse = provisioners.GetSchemaResponse{ 381 Provisioner: &configschema.Block{ 382 Attributes: map[string]*configschema.Attribute{ 383 "command": { 384 Type: cty.String, 385 Optional: true, 386 }, 387 "order": { 388 Type: cty.String, 389 Optional: true, 390 }, 391 "when": { 392 Type: cty.String, 393 Optional: true, 394 }, 395 }, 396 }, 397 } 398 return p 399 } 400 401 func checkStateString(t *testing.T, state *states.State, expected string) { 402 t.Helper() 403 actual := strings.TrimSpace(state.String()) 404 expected = strings.TrimSpace(expected) 405 406 if actual != expected { 407 t.Fatalf("incorrect state\ngot:\n%s\n\nwant:\n%s", actual, expected) 408 } 409 } 410 411 // Test helper that gives a function 3 seconds to finish, assumes deadlock and 412 // fails test if it does not. 413 func testCheckDeadlock(t *testing.T, f func()) { 414 t.Helper() 415 timeout := make(chan bool, 1) 416 done := make(chan bool, 1) 417 go func() { 418 time.Sleep(3 * time.Second) 419 timeout <- true 420 }() 421 go func(f func(), done chan bool) { 422 defer func() { done <- true }() 423 f() 424 }(f, done) 425 select { 426 case <-timeout: 427 t.Fatalf("timed out! probably deadlock") 428 case <-done: 429 // ok 430 } 431 } 432 433 func testProviderSchema(name string) *providers.GetProviderSchemaResponse { 434 return getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 435 Provider: &configschema.Block{ 436 Attributes: map[string]*configschema.Attribute{ 437 "region": { 438 Type: cty.String, 439 Optional: true, 440 }, 441 "foo": { 442 Type: cty.String, 443 Optional: true, 444 }, 445 "value": { 446 Type: cty.String, 447 Optional: true, 448 }, 449 "root": { 450 Type: cty.Number, 451 Optional: true, 452 }, 453 }, 454 }, 455 ResourceTypes: map[string]*configschema.Block{ 456 name + "_instance": { 457 Attributes: map[string]*configschema.Attribute{ 458 "id": { 459 Type: cty.String, 460 Computed: true, 461 }, 462 "ami": { 463 Type: cty.String, 464 Optional: true, 465 }, 466 "dep": { 467 Type: cty.String, 468 Optional: true, 469 }, 470 "num": { 471 Type: cty.Number, 472 Optional: true, 473 }, 474 "require_new": { 475 Type: cty.String, 476 Optional: true, 477 }, 478 "var": { 479 Type: cty.String, 480 Optional: true, 481 }, 482 "foo": { 483 Type: cty.String, 484 Optional: true, 485 Computed: true, 486 }, 487 "bar": { 488 Type: cty.String, 489 Optional: true, 490 }, 491 "compute": { 492 Type: cty.String, 493 Optional: true, 494 Computed: false, 495 }, 496 "compute_value": { 497 Type: cty.String, 498 Optional: true, 499 Computed: true, 500 }, 501 "value": { 502 Type: cty.String, 503 Optional: true, 504 Computed: true, 505 }, 506 "output": { 507 Type: cty.String, 508 Optional: true, 509 }, 510 "write": { 511 Type: cty.String, 512 Optional: true, 513 }, 514 "instance": { 515 Type: cty.String, 516 Optional: true, 517 }, 518 "vpc_id": { 519 Type: cty.String, 520 Optional: true, 521 }, 522 "type": { 523 Type: cty.String, 524 Computed: true, 525 }, 526 527 // Generated by testDiffFn if compute = "unknown" is set in the test config 528 "unknown": { 529 Type: cty.String, 530 Computed: true, 531 }, 532 }, 533 }, 534 name + "_eip": { 535 Attributes: map[string]*configschema.Attribute{ 536 "id": { 537 Type: cty.String, 538 Computed: true, 539 }, 540 "instance": { 541 Type: cty.String, 542 Optional: true, 543 }, 544 }, 545 }, 546 name + "_resource": { 547 Attributes: map[string]*configschema.Attribute{ 548 "id": { 549 Type: cty.String, 550 Computed: true, 551 }, 552 "value": { 553 Type: cty.String, 554 Optional: true, 555 }, 556 "sensitive_value": { 557 Type: cty.String, 558 Sensitive: true, 559 Optional: true, 560 }, 561 "random": { 562 Type: cty.String, 563 Optional: true, 564 }, 565 }, 566 BlockTypes: map[string]*configschema.NestedBlock{ 567 "nesting_single": { 568 Block: configschema.Block{ 569 Attributes: map[string]*configschema.Attribute{ 570 "value": {Type: cty.String, Optional: true}, 571 "sensitive_value": {Type: cty.String, Optional: true, Sensitive: true}, 572 }, 573 }, 574 Nesting: configschema.NestingSingle, 575 }, 576 }, 577 }, 578 name + "_ami_list": { 579 Attributes: map[string]*configschema.Attribute{ 580 "id": { 581 Type: cty.String, 582 Optional: true, 583 Computed: true, 584 }, 585 "ids": { 586 Type: cty.List(cty.String), 587 Optional: true, 588 Computed: true, 589 }, 590 }, 591 }, 592 name + "_remote_state": { 593 Attributes: map[string]*configschema.Attribute{ 594 "id": { 595 Type: cty.String, 596 Optional: true, 597 }, 598 "foo": { 599 Type: cty.String, 600 Optional: true, 601 }, 602 "output": { 603 Type: cty.Map(cty.String), 604 Computed: true, 605 }, 606 }, 607 }, 608 name + "_file": { 609 Attributes: map[string]*configschema.Attribute{ 610 "id": { 611 Type: cty.String, 612 Optional: true, 613 }, 614 "template": { 615 Type: cty.String, 616 Optional: true, 617 }, 618 "rendered": { 619 Type: cty.String, 620 Computed: true, 621 }, 622 "__template_requires_new": { 623 Type: cty.String, 624 Optional: true, 625 }, 626 }, 627 }, 628 }, 629 DataSources: map[string]*configschema.Block{ 630 name + "_data_source": { 631 Attributes: map[string]*configschema.Attribute{ 632 "id": { 633 Type: cty.String, 634 Computed: true, 635 }, 636 "foo": { 637 Type: cty.String, 638 Optional: true, 639 Computed: true, 640 }, 641 }, 642 }, 643 name + "_remote_state": { 644 Attributes: map[string]*configschema.Attribute{ 645 "id": { 646 Type: cty.String, 647 Optional: true, 648 }, 649 "foo": { 650 Type: cty.String, 651 Optional: true, 652 }, 653 "output": { 654 Type: cty.Map(cty.String), 655 Optional: true, 656 }, 657 }, 658 }, 659 name + "_file": { 660 Attributes: map[string]*configschema.Attribute{ 661 "id": { 662 Type: cty.String, 663 Optional: true, 664 }, 665 "template": { 666 Type: cty.String, 667 Optional: true, 668 }, 669 "rendered": { 670 Type: cty.String, 671 Computed: true, 672 }, 673 }, 674 }, 675 name + "_sensitive_data_source": { 676 Attributes: map[string]*configschema.Attribute{ 677 "id": { 678 Type: cty.String, 679 Computed: true, 680 }, 681 "value": { 682 Type: cty.String, 683 Optional: true, 684 Sensitive: true, 685 }, 686 }, 687 }, 688 }, 689 }) 690 } 691 692 // contextOptsForPlanViaFile is a helper that creates a temporary plan file, 693 // then reads it back in again and produces a ContextOpts object containing the 694 // planned changes, prior state and config from the plan file. 695 // 696 // This is intended for testing the separated plan/apply workflow in a more 697 // convenient way than spelling out all of these steps every time. Normally 698 // only the command and backend packages need to deal with such things, but 699 // our context tests try to exercise lots of stuff at once and so having them 700 // round-trip things through on-disk files is often an important part of 701 // fully representing an old bug in a regression test. 702 func contextOptsForPlanViaFile(t *testing.T, configSnap *configload.Snapshot, plan *plans.Plan) (*ContextOpts, *configs.Config, *plans.Plan, error) { 703 dir := t.TempDir() 704 705 // We'll just create a dummy statefile.File here because we're not going 706 // to run through any of the codepaths that care about Lineage/Serial/etc 707 // here anyway. 708 stateFile := &statefile.File{ 709 State: plan.PriorState, 710 } 711 prevStateFile := &statefile.File{ 712 State: plan.PrevRunState, 713 } 714 715 // To make life a little easier for test authors, we'll populate a simple 716 // backend configuration if they didn't set one, since the backend is 717 // usually dealt with in a calling package and so tests in this package 718 // don't really care about it. 719 if plan.Backend.Config == nil { 720 cfg, err := plans.NewDynamicValue(cty.EmptyObjectVal, cty.EmptyObject) 721 if err != nil { 722 panic(fmt.Sprintf("NewDynamicValue failed: %s", err)) // shouldn't happen because we control the inputs 723 } 724 plan.Backend.Type = "local" 725 plan.Backend.Config = cfg 726 plan.Backend.Workspace = "default" 727 } 728 729 filename := filepath.Join(dir, "tfplan") 730 err := planfile.Create(filename, planfile.CreateArgs{ 731 ConfigSnapshot: configSnap, 732 PreviousRunStateFile: prevStateFile, 733 StateFile: stateFile, 734 Plan: plan, 735 }, encryption.PlanEncryptionDisabled()) 736 if err != nil { 737 return nil, nil, nil, err 738 } 739 740 pr, err := planfile.Open(filename, encryption.PlanEncryptionDisabled()) 741 if err != nil { 742 return nil, nil, nil, err 743 } 744 745 config, diags := pr.ReadConfig() 746 if diags.HasErrors() { 747 return nil, nil, nil, diags.Err() 748 } 749 750 plan, err = pr.ReadPlan() 751 if err != nil { 752 return nil, nil, nil, err 753 } 754 755 // Note: This has grown rather silly over the course of ongoing refactoring, 756 // because ContextOpts is no longer actually responsible for carrying 757 // any information from a plan file and instead all of the information 758 // lives inside the config and plan objects. We continue to return a 759 // silly empty ContextOpts here just to keep all of the calling tests 760 // working. 761 return &ContextOpts{}, config, plan, nil 762 } 763 764 // legacyPlanComparisonString produces a string representation of the changes 765 // from a plan and a given state togther, as was formerly produced by the 766 // String method of tofu.Plan. 767 // 768 // This is here only for compatibility with existing tests that predate our 769 // new plan and state types, and should not be used in new tests. Instead, use 770 // a library like "cmp" to do a deep equality check and diff on the two 771 // data structures. 772 func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string { 773 return fmt.Sprintf( 774 "DIFF:\n\n%s\n\nSTATE:\n\n%s", 775 legacyDiffComparisonString(changes), 776 state.String(), 777 ) 778 } 779 780 // legacyDiffComparisonString produces a string representation of the changes 781 // from a planned changes object, as was formerly produced by the String method 782 // of tofu.Diff. 783 // 784 // This is here only for compatibility with existing tests that predate our 785 // new plan types, and should not be used in new tests. Instead, use a library 786 // like "cmp" to do a deep equality check and diff on the two data structures. 787 func legacyDiffComparisonString(changes *plans.Changes) string { 788 // The old string representation of a plan was grouped by module, but 789 // our new plan structure is not grouped in that way and so we'll need 790 // to preprocess it in order to produce that grouping. 791 type ResourceChanges struct { 792 Current *plans.ResourceInstanceChangeSrc 793 Deposed map[states.DeposedKey]*plans.ResourceInstanceChangeSrc 794 } 795 byModule := map[string]map[string]*ResourceChanges{} 796 resourceKeys := map[string][]string{} 797 var moduleKeys []string 798 for _, rc := range changes.Resources { 799 if rc.Action == plans.NoOp { 800 // We won't mention no-op changes here at all, since the old plan 801 // model we are emulating here didn't have such a concept. 802 continue 803 } 804 moduleKey := rc.Addr.Module.String() 805 if _, exists := byModule[moduleKey]; !exists { 806 moduleKeys = append(moduleKeys, moduleKey) 807 byModule[moduleKey] = make(map[string]*ResourceChanges) 808 } 809 resourceKey := rc.Addr.Resource.String() 810 if _, exists := byModule[moduleKey][resourceKey]; !exists { 811 resourceKeys[moduleKey] = append(resourceKeys[moduleKey], resourceKey) 812 byModule[moduleKey][resourceKey] = &ResourceChanges{ 813 Deposed: make(map[states.DeposedKey]*plans.ResourceInstanceChangeSrc), 814 } 815 } 816 817 if rc.DeposedKey == states.NotDeposed { 818 byModule[moduleKey][resourceKey].Current = rc 819 } else { 820 byModule[moduleKey][resourceKey].Deposed[rc.DeposedKey] = rc 821 } 822 } 823 sort.Strings(moduleKeys) 824 for _, ks := range resourceKeys { 825 sort.Strings(ks) 826 } 827 828 var buf bytes.Buffer 829 830 for _, moduleKey := range moduleKeys { 831 rcs := byModule[moduleKey] 832 var mBuf bytes.Buffer 833 834 for _, resourceKey := range resourceKeys[moduleKey] { 835 rc := rcs[resourceKey] 836 837 crud := "UPDATE" 838 if rc.Current != nil { 839 switch rc.Current.Action { 840 case plans.DeleteThenCreate: 841 crud = "DESTROY/CREATE" 842 case plans.CreateThenDelete: 843 crud = "CREATE/DESTROY" 844 case plans.Delete: 845 crud = "DESTROY" 846 case plans.Create: 847 crud = "CREATE" 848 } 849 } else { 850 // We must be working on a deposed object then, in which 851 // case destroying is the only possible action. 852 crud = "DESTROY" 853 } 854 855 extra := "" 856 if rc.Current == nil && len(rc.Deposed) > 0 { 857 extra = " (deposed only)" 858 } 859 860 fmt.Fprintf( 861 &mBuf, "%s: %s%s\n", 862 crud, resourceKey, extra, 863 ) 864 865 attrNames := map[string]bool{} 866 var oldAttrs map[string]string 867 var newAttrs map[string]string 868 if rc.Current != nil { 869 if before := rc.Current.Before; before != nil { 870 ty, err := before.ImpliedType() 871 if err == nil { 872 val, err := before.Decode(ty) 873 if err == nil { 874 oldAttrs = hcl2shim.FlatmapValueFromHCL2(val) 875 for k := range oldAttrs { 876 attrNames[k] = true 877 } 878 } 879 } 880 } 881 if after := rc.Current.After; after != nil { 882 ty, err := after.ImpliedType() 883 if err == nil { 884 val, err := after.Decode(ty) 885 if err == nil { 886 newAttrs = hcl2shim.FlatmapValueFromHCL2(val) 887 for k := range newAttrs { 888 attrNames[k] = true 889 } 890 } 891 } 892 } 893 } 894 if oldAttrs == nil { 895 oldAttrs = make(map[string]string) 896 } 897 if newAttrs == nil { 898 newAttrs = make(map[string]string) 899 } 900 901 attrNamesOrder := make([]string, 0, len(attrNames)) 902 keyLen := 0 903 for n := range attrNames { 904 attrNamesOrder = append(attrNamesOrder, n) 905 if len(n) > keyLen { 906 keyLen = len(n) 907 } 908 } 909 sort.Strings(attrNamesOrder) 910 911 for _, attrK := range attrNamesOrder { 912 v := newAttrs[attrK] 913 u := oldAttrs[attrK] 914 915 if v == hcl2shim.UnknownVariableValue { 916 v = "<computed>" 917 } 918 // NOTE: we don't support <sensitive> here because we would 919 // need schema to do that. Excluding sensitive values 920 // is now done at the UI layer, and so should not be tested 921 // at the core layer. 922 923 updateMsg := "" 924 // TODO: Mark " (forces new resource)" in updateMsg when appropriate. 925 926 fmt.Fprintf( 927 &mBuf, " %s:%s %#v => %#v%s\n", 928 attrK, 929 strings.Repeat(" ", keyLen-len(attrK)), 930 u, v, 931 updateMsg, 932 ) 933 } 934 } 935 936 if moduleKey == "" { // root module 937 buf.Write(mBuf.Bytes()) 938 buf.WriteByte('\n') 939 continue 940 } 941 942 fmt.Fprintf(&buf, "%s:\n", moduleKey) 943 s := bufio.NewScanner(&mBuf) 944 for s.Scan() { 945 buf.WriteString(fmt.Sprintf(" %s\n", s.Text())) 946 } 947 } 948 949 return buf.String() 950 } 951 952 // assertNoDiagnostics fails the test in progress (using t.Fatal) if the given 953 // diagnostics is non-empty. 954 func assertNoDiagnostics(t *testing.T, diags tfdiags.Diagnostics) { 955 t.Helper() 956 if len(diags) == 0 { 957 return 958 } 959 logDiagnostics(t, diags) 960 t.FailNow() 961 } 962 963 // assertNoDiagnostics fails the test in progress (using t.Fatal) if the given 964 // diagnostics has any errors. 965 func assertNoErrors(t *testing.T, diags tfdiags.Diagnostics) { 966 t.Helper() 967 if !diags.HasErrors() { 968 return 969 } 970 logDiagnostics(t, diags) 971 t.FailNow() 972 } 973 974 // assertDiagnosticsMatch fails the test in progress (using t.Fatal) if the 975 // two sets of diagnostics don't match after being normalized using the 976 // "ForRPC" processing step, which eliminates the specific type information 977 // and HCL expression information of each diagnostic. 978 // 979 // assertDiagnosticsMatch sorts the two sets of diagnostics in the usual way 980 // before comparing them, though diagnostics only have a partial order so that 981 // will not totally normalize the ordering of all diagnostics sets. 982 func assertDiagnosticsMatch(t *testing.T, got, want tfdiags.Diagnostics) { 983 got = got.ForRPC() 984 want = want.ForRPC() 985 got.Sort() 986 want.Sort() 987 if diff := cmp.Diff(want, got); diff != "" { 988 t.Fatalf("wrong diagnostics\n%s", diff) 989 } 990 } 991 992 // logDiagnostics is a test helper that logs the given diagnostics to to the 993 // given testing.T using t.Log, in a way that is hopefully useful in debugging 994 // a test. It does not generate any errors or fail the test. See 995 // assertNoDiagnostics and assertNoErrors for more specific helpers that can 996 // also fail the test. 997 func logDiagnostics(t *testing.T, diags tfdiags.Diagnostics) { 998 t.Helper() 999 for _, diag := range diags { 1000 desc := diag.Description() 1001 rng := diag.Source() 1002 1003 var severity string 1004 switch diag.Severity() { 1005 case tfdiags.Error: 1006 severity = "ERROR" 1007 case tfdiags.Warning: 1008 severity = "WARN" 1009 default: 1010 severity = "???" // should never happen 1011 } 1012 1013 if subj := rng.Subject; subj != nil { 1014 if desc.Detail == "" { 1015 t.Logf("[%s@%s] %s", severity, subj.StartString(), desc.Summary) 1016 } else { 1017 t.Logf("[%s@%s] %s: %s", severity, subj.StartString(), desc.Summary, desc.Detail) 1018 } 1019 } else { 1020 if desc.Detail == "" { 1021 t.Logf("[%s] %s", severity, desc.Summary) 1022 } else { 1023 t.Logf("[%s] %s: %s", severity, desc.Summary, desc.Detail) 1024 } 1025 } 1026 } 1027 } 1028 1029 const testContextRefreshModuleStr = ` 1030 aws_instance.web: (tainted) 1031 ID = bar 1032 provider = provider["registry.opentofu.org/hashicorp/aws"] 1033 1034 module.child: 1035 aws_instance.web: 1036 ID = new 1037 provider = provider["registry.opentofu.org/hashicorp/aws"] 1038 ` 1039 1040 const testContextRefreshOutputStr = ` 1041 aws_instance.web: 1042 ID = foo 1043 provider = provider["registry.opentofu.org/hashicorp/aws"] 1044 foo = bar 1045 1046 Outputs: 1047 1048 foo = bar 1049 ` 1050 1051 const testContextRefreshOutputPartialStr = ` 1052 <no state> 1053 ` 1054 1055 const testContextRefreshTaintedStr = ` 1056 aws_instance.web: (tainted) 1057 ID = foo 1058 provider = provider["registry.opentofu.org/hashicorp/aws"] 1059 `