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