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