github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/command/views/operation_test.go (about) 1 package views 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "strings" 7 "testing" 8 9 "github.com/hashicorp/terraform/internal/addrs" 10 "github.com/hashicorp/terraform/internal/command/arguments" 11 "github.com/hashicorp/terraform/internal/lang/globalref" 12 "github.com/hashicorp/terraform/internal/plans" 13 "github.com/hashicorp/terraform/internal/states" 14 "github.com/hashicorp/terraform/internal/states/statefile" 15 "github.com/hashicorp/terraform/internal/terminal" 16 "github.com/hashicorp/terraform/internal/terraform" 17 "github.com/zclconf/go-cty/cty" 18 ) 19 20 func TestOperation_stopping(t *testing.T) { 21 streams, done := terminal.StreamsForTesting(t) 22 v := NewOperation(arguments.ViewHuman, false, NewView(streams)) 23 24 v.Stopping() 25 26 if got, want := done(t).Stdout(), "Stopping operation...\n"; got != want { 27 t.Errorf("wrong result\ngot: %q\nwant: %q", got, want) 28 } 29 } 30 31 func TestOperation_cancelled(t *testing.T) { 32 testCases := map[string]struct { 33 planMode plans.Mode 34 want string 35 }{ 36 "apply": { 37 planMode: plans.NormalMode, 38 want: "Apply cancelled.\n", 39 }, 40 "destroy": { 41 planMode: plans.DestroyMode, 42 want: "Destroy cancelled.\n", 43 }, 44 } 45 for name, tc := range testCases { 46 t.Run(name, func(t *testing.T) { 47 streams, done := terminal.StreamsForTesting(t) 48 v := NewOperation(arguments.ViewHuman, false, NewView(streams)) 49 50 v.Cancelled(tc.planMode) 51 52 if got, want := done(t).Stdout(), tc.want; got != want { 53 t.Errorf("wrong result\ngot: %q\nwant: %q", got, want) 54 } 55 }) 56 } 57 } 58 59 func TestOperation_emergencyDumpState(t *testing.T) { 60 streams, done := terminal.StreamsForTesting(t) 61 v := NewOperation(arguments.ViewHuman, false, NewView(streams)) 62 63 stateFile := statefile.New(nil, "foo", 1) 64 65 err := v.EmergencyDumpState(stateFile) 66 if err != nil { 67 t.Fatalf("unexpected error dumping state: %s", err) 68 } 69 70 // Check that the result (on stderr) looks like JSON state 71 raw := done(t).Stderr() 72 var state map[string]interface{} 73 if err := json.Unmarshal([]byte(raw), &state); err != nil { 74 t.Fatalf("unexpected error parsing dumped state: %s\nraw:\n%s", err, raw) 75 } 76 } 77 78 func TestOperation_planNoChanges(t *testing.T) { 79 80 tests := map[string]struct { 81 plan func(schemas *terraform.Schemas) *plans.Plan 82 wantText string 83 }{ 84 "nothing at all in normal mode": { 85 func(schemas *terraform.Schemas) *plans.Plan { 86 return &plans.Plan{ 87 UIMode: plans.NormalMode, 88 Changes: plans.NewChanges(), 89 } 90 }, 91 "no differences, so no changes are needed.", 92 }, 93 "nothing at all in refresh-only mode": { 94 func(schemas *terraform.Schemas) *plans.Plan { 95 return &plans.Plan{ 96 UIMode: plans.RefreshOnlyMode, 97 Changes: plans.NewChanges(), 98 } 99 }, 100 "Terraform has checked that the real remote objects still match", 101 }, 102 "nothing at all in destroy mode": { 103 func(schemas *terraform.Schemas) *plans.Plan { 104 return &plans.Plan{ 105 UIMode: plans.DestroyMode, 106 Changes: plans.NewChanges(), 107 } 108 }, 109 "No objects need to be destroyed.", 110 }, 111 "no drift detected in normal noop": { 112 func(schemas *terraform.Schemas) *plans.Plan { 113 addr := addrs.Resource{ 114 Mode: addrs.ManagedResourceMode, 115 Type: "test_resource", 116 Name: "somewhere", 117 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 118 schema, _ := schemas.ResourceTypeConfig( 119 addrs.NewDefaultProvider("test"), 120 addr.Resource.Resource.Mode, 121 addr.Resource.Resource.Type, 122 ) 123 ty := schema.ImpliedType() 124 rc := &plans.ResourceInstanceChange{ 125 Addr: addr, 126 PrevRunAddr: addr, 127 ProviderAddr: addrs.RootModuleInstance.ProviderConfigDefault( 128 addrs.NewDefaultProvider("test"), 129 ), 130 Change: plans.Change{ 131 Action: plans.Update, 132 Before: cty.NullVal(ty), 133 After: cty.ObjectVal(map[string]cty.Value{ 134 "id": cty.StringVal("1234"), 135 "foo": cty.StringVal("bar"), 136 }), 137 }, 138 } 139 rcs, err := rc.Encode(ty) 140 if err != nil { 141 panic(err) 142 } 143 drs := []*plans.ResourceInstanceChangeSrc{rcs} 144 return &plans.Plan{ 145 UIMode: plans.NormalMode, 146 Changes: plans.NewChanges(), 147 DriftedResources: drs, 148 } 149 }, 150 "No changes", 151 }, 152 "drift detected in normal mode": { 153 func(schemas *terraform.Schemas) *plans.Plan { 154 addr := addrs.Resource{ 155 Mode: addrs.ManagedResourceMode, 156 Type: "test_resource", 157 Name: "somewhere", 158 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 159 schema, _ := schemas.ResourceTypeConfig( 160 addrs.NewDefaultProvider("test"), 161 addr.Resource.Resource.Mode, 162 addr.Resource.Resource.Type, 163 ) 164 ty := schema.ImpliedType() 165 rc := &plans.ResourceInstanceChange{ 166 Addr: addr, 167 PrevRunAddr: addr, 168 ProviderAddr: addrs.RootModuleInstance.ProviderConfigDefault( 169 addrs.NewDefaultProvider("test"), 170 ), 171 Change: plans.Change{ 172 Action: plans.Update, 173 Before: cty.NullVal(ty), 174 After: cty.ObjectVal(map[string]cty.Value{ 175 "id": cty.StringVal("1234"), 176 "foo": cty.StringVal("bar"), 177 }), 178 }, 179 } 180 rcs, err := rc.Encode(ty) 181 if err != nil { 182 panic(err) 183 } 184 drs := []*plans.ResourceInstanceChangeSrc{rcs} 185 changes := plans.NewChanges() 186 changes.Resources = drs 187 return &plans.Plan{ 188 UIMode: plans.NormalMode, 189 Changes: changes, 190 DriftedResources: drs, 191 RelevantAttributes: []globalref.ResourceAttr{{ 192 Resource: addr, 193 Attr: cty.GetAttrPath("id"), 194 }}, 195 } 196 }, 197 "Objects have changed outside of Terraform", 198 }, 199 "drift detected in refresh-only mode": { 200 func(schemas *terraform.Schemas) *plans.Plan { 201 addr := addrs.Resource{ 202 Mode: addrs.ManagedResourceMode, 203 Type: "test_resource", 204 Name: "somewhere", 205 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 206 schema, _ := schemas.ResourceTypeConfig( 207 addrs.NewDefaultProvider("test"), 208 addr.Resource.Resource.Mode, 209 addr.Resource.Resource.Type, 210 ) 211 ty := schema.ImpliedType() 212 rc := &plans.ResourceInstanceChange{ 213 Addr: addr, 214 PrevRunAddr: addr, 215 ProviderAddr: addrs.RootModuleInstance.ProviderConfigDefault( 216 addrs.NewDefaultProvider("test"), 217 ), 218 Change: plans.Change{ 219 Action: plans.Update, 220 Before: cty.NullVal(ty), 221 After: cty.ObjectVal(map[string]cty.Value{ 222 "id": cty.StringVal("1234"), 223 "foo": cty.StringVal("bar"), 224 }), 225 }, 226 } 227 rcs, err := rc.Encode(ty) 228 if err != nil { 229 panic(err) 230 } 231 drs := []*plans.ResourceInstanceChangeSrc{rcs} 232 return &plans.Plan{ 233 UIMode: plans.RefreshOnlyMode, 234 Changes: plans.NewChanges(), 235 DriftedResources: drs, 236 } 237 }, 238 "If you were expecting these changes then you can apply this plan", 239 }, 240 "move-only changes in refresh-only mode": { 241 func(schemas *terraform.Schemas) *plans.Plan { 242 addr := addrs.Resource{ 243 Mode: addrs.ManagedResourceMode, 244 Type: "test_resource", 245 Name: "somewhere", 246 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 247 addrPrev := addrs.Resource{ 248 Mode: addrs.ManagedResourceMode, 249 Type: "test_resource", 250 Name: "anywhere", 251 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 252 schema, _ := schemas.ResourceTypeConfig( 253 addrs.NewDefaultProvider("test"), 254 addr.Resource.Resource.Mode, 255 addr.Resource.Resource.Type, 256 ) 257 ty := schema.ImpliedType() 258 rc := &plans.ResourceInstanceChange{ 259 Addr: addr, 260 PrevRunAddr: addrPrev, 261 ProviderAddr: addrs.RootModuleInstance.ProviderConfigDefault( 262 addrs.NewDefaultProvider("test"), 263 ), 264 Change: plans.Change{ 265 Action: plans.NoOp, 266 Before: cty.ObjectVal(map[string]cty.Value{ 267 "id": cty.StringVal("1234"), 268 "foo": cty.StringVal("bar"), 269 }), 270 After: cty.ObjectVal(map[string]cty.Value{ 271 "id": cty.StringVal("1234"), 272 "foo": cty.StringVal("bar"), 273 }), 274 }, 275 } 276 rcs, err := rc.Encode(ty) 277 if err != nil { 278 panic(err) 279 } 280 drs := []*plans.ResourceInstanceChangeSrc{rcs} 281 return &plans.Plan{ 282 UIMode: plans.RefreshOnlyMode, 283 Changes: plans.NewChanges(), 284 DriftedResources: drs, 285 } 286 }, 287 "test_resource.anywhere has moved to test_resource.somewhere", 288 }, 289 "drift detected in destroy mode": { 290 func(schemas *terraform.Schemas) *plans.Plan { 291 return &plans.Plan{ 292 UIMode: plans.DestroyMode, 293 Changes: plans.NewChanges(), 294 PrevRunState: states.BuildState(func(state *states.SyncState) { 295 state.SetResourceInstanceCurrent( 296 addrs.Resource{ 297 Mode: addrs.ManagedResourceMode, 298 Type: "test_resource", 299 Name: "somewhere", 300 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 301 &states.ResourceInstanceObjectSrc{ 302 Status: states.ObjectReady, 303 AttrsJSON: []byte(`{}`), 304 }, 305 addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 306 ) 307 }), 308 PriorState: states.NewState(), 309 } 310 }, 311 "No objects need to be destroyed.", 312 }, 313 } 314 315 schemas := testSchemas() 316 for name, test := range tests { 317 t.Run(name, func(t *testing.T) { 318 streams, done := terminal.StreamsForTesting(t) 319 v := NewOperation(arguments.ViewHuman, false, NewView(streams)) 320 plan := test.plan(schemas) 321 v.Plan(plan, schemas) 322 got := done(t).Stdout() 323 if want := test.wantText; want != "" && !strings.Contains(got, want) { 324 t.Errorf("missing expected message\ngot:\n%s\n\nwant substring: %s", got, want) 325 } 326 }) 327 } 328 } 329 330 func TestOperation_plan(t *testing.T) { 331 streams, done := terminal.StreamsForTesting(t) 332 v := NewOperation(arguments.ViewHuman, true, NewView(streams)) 333 334 plan := testPlan(t) 335 schemas := testSchemas() 336 v.Plan(plan, schemas) 337 338 want := ` 339 Terraform used the selected providers to generate the following execution 340 plan. Resource actions are indicated with the following symbols: 341 + create 342 343 Terraform will perform the following actions: 344 345 # test_resource.foo will be created 346 + resource "test_resource" "foo" { 347 + foo = "bar" 348 + id = (known after apply) 349 } 350 351 Plan: 1 to add, 0 to change, 0 to destroy. 352 ` 353 354 if got := done(t).Stdout(); got != want { 355 t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s", got, want) 356 } 357 } 358 359 func TestOperation_planWithDatasource(t *testing.T) { 360 streams, done := terminal.StreamsForTesting(t) 361 v := NewOperation(arguments.ViewHuman, true, NewView(streams)) 362 363 plan := testPlanWithDatasource(t) 364 schemas := testSchemas() 365 v.Plan(plan, schemas) 366 367 want := ` 368 Terraform used the selected providers to generate the following execution 369 plan. Resource actions are indicated with the following symbols: 370 + create 371 <= read (data resources) 372 373 Terraform will perform the following actions: 374 375 # data.test_data_source.bar will be read during apply 376 <= data "test_data_source" "bar" { 377 + bar = "foo" 378 + id = "C6743020-40BD-4591-81E6-CD08494341D3" 379 } 380 381 # test_resource.foo will be created 382 + resource "test_resource" "foo" { 383 + foo = "bar" 384 + id = (known after apply) 385 } 386 387 Plan: 1 to add, 0 to change, 0 to destroy. 388 ` 389 390 if got := done(t).Stdout(); got != want { 391 t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s", got, want) 392 } 393 } 394 395 func TestOperation_planWithDatasourceAndDrift(t *testing.T) { 396 streams, done := terminal.StreamsForTesting(t) 397 v := NewOperation(arguments.ViewHuman, true, NewView(streams)) 398 399 plan := testPlanWithDatasource(t) 400 schemas := testSchemas() 401 v.Plan(plan, schemas) 402 403 want := ` 404 Terraform used the selected providers to generate the following execution 405 plan. Resource actions are indicated with the following symbols: 406 + create 407 <= read (data resources) 408 409 Terraform will perform the following actions: 410 411 # data.test_data_source.bar will be read during apply 412 <= data "test_data_source" "bar" { 413 + bar = "foo" 414 + id = "C6743020-40BD-4591-81E6-CD08494341D3" 415 } 416 417 # test_resource.foo will be created 418 + resource "test_resource" "foo" { 419 + foo = "bar" 420 + id = (known after apply) 421 } 422 423 Plan: 1 to add, 0 to change, 0 to destroy. 424 ` 425 426 if got := done(t).Stdout(); got != want { 427 t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s", got, want) 428 } 429 } 430 431 func TestOperation_planNextStep(t *testing.T) { 432 testCases := map[string]struct { 433 path string 434 want string 435 }{ 436 "no state path": { 437 path: "", 438 want: "You didn't use the -out option", 439 }, 440 "state path": { 441 path: "good plan.tfplan", 442 want: `terraform apply "good plan.tfplan"`, 443 }, 444 } 445 for name, tc := range testCases { 446 t.Run(name, func(t *testing.T) { 447 streams, done := terminal.StreamsForTesting(t) 448 v := NewOperation(arguments.ViewHuman, false, NewView(streams)) 449 450 v.PlanNextStep(tc.path) 451 452 if got := done(t).Stdout(); !strings.Contains(got, tc.want) { 453 t.Errorf("wrong result\ngot: %q\nwant: %q", got, tc.want) 454 } 455 }) 456 } 457 } 458 459 // The in-automation state is on the view itself, so testing it separately is 460 // clearer. 461 func TestOperation_planNextStepInAutomation(t *testing.T) { 462 streams, done := terminal.StreamsForTesting(t) 463 v := NewOperation(arguments.ViewHuman, true, NewView(streams)) 464 465 v.PlanNextStep("") 466 467 if got := done(t).Stdout(); got != "" { 468 t.Errorf("unexpected output\ngot: %q", got) 469 } 470 } 471 472 // Test all the trivial OperationJSON methods together. Y'know, for brevity. 473 // This test is not a realistic stream of messages. 474 func TestOperationJSON_logs(t *testing.T) { 475 streams, done := terminal.StreamsForTesting(t) 476 v := &OperationJSON{view: NewJSONView(NewView(streams))} 477 478 v.Cancelled(plans.NormalMode) 479 v.Cancelled(plans.DestroyMode) 480 v.Stopping() 481 v.Interrupted() 482 v.FatalInterrupt() 483 484 want := []map[string]interface{}{ 485 { 486 "@level": "info", 487 "@message": "Apply cancelled", 488 "@module": "terraform.ui", 489 "type": "log", 490 }, 491 { 492 "@level": "info", 493 "@message": "Destroy cancelled", 494 "@module": "terraform.ui", 495 "type": "log", 496 }, 497 { 498 "@level": "info", 499 "@message": "Stopping operation...", 500 "@module": "terraform.ui", 501 "type": "log", 502 }, 503 { 504 "@level": "info", 505 "@message": interrupted, 506 "@module": "terraform.ui", 507 "type": "log", 508 }, 509 { 510 "@level": "info", 511 "@message": fatalInterrupt, 512 "@module": "terraform.ui", 513 "type": "log", 514 }, 515 } 516 517 testJSONViewOutputEquals(t, done(t).Stdout(), want) 518 } 519 520 // This is a fairly circular test, but it's such a rarely executed code path 521 // that I think it's probably still worth having. We're not testing against 522 // a fixed state JSON output because this test ought not fail just because 523 // we upgrade state format in the future. 524 func TestOperationJSON_emergencyDumpState(t *testing.T) { 525 streams, done := terminal.StreamsForTesting(t) 526 v := &OperationJSON{view: NewJSONView(NewView(streams))} 527 528 stateFile := statefile.New(nil, "foo", 1) 529 stateBuf := new(bytes.Buffer) 530 err := statefile.Write(stateFile, stateBuf) 531 if err != nil { 532 t.Fatal(err) 533 } 534 var stateJSON map[string]interface{} 535 err = json.Unmarshal(stateBuf.Bytes(), &stateJSON) 536 if err != nil { 537 t.Fatal(err) 538 } 539 540 err = v.EmergencyDumpState(stateFile) 541 if err != nil { 542 t.Fatalf("unexpected error dumping state: %s", err) 543 } 544 545 want := []map[string]interface{}{ 546 { 547 "@level": "info", 548 "@message": "Emergency state dump", 549 "@module": "terraform.ui", 550 "type": "log", 551 "state": stateJSON, 552 }, 553 } 554 555 testJSONViewOutputEquals(t, done(t).Stdout(), want) 556 } 557 558 func TestOperationJSON_planNoChanges(t *testing.T) { 559 streams, done := terminal.StreamsForTesting(t) 560 v := &OperationJSON{view: NewJSONView(NewView(streams))} 561 562 plan := &plans.Plan{ 563 Changes: plans.NewChanges(), 564 } 565 v.Plan(plan, nil) 566 567 want := []map[string]interface{}{ 568 { 569 "@level": "info", 570 "@message": "Plan: 0 to add, 0 to change, 0 to destroy.", 571 "@module": "terraform.ui", 572 "type": "change_summary", 573 "changes": map[string]interface{}{ 574 "operation": "plan", 575 "add": float64(0), 576 "change": float64(0), 577 "remove": float64(0), 578 }, 579 }, 580 } 581 582 testJSONViewOutputEquals(t, done(t).Stdout(), want) 583 } 584 585 func TestOperationJSON_plan(t *testing.T) { 586 streams, done := terminal.StreamsForTesting(t) 587 v := &OperationJSON{view: NewJSONView(NewView(streams))} 588 589 root := addrs.RootModuleInstance 590 vpc, diags := addrs.ParseModuleInstanceStr("module.vpc") 591 if len(diags) > 0 { 592 t.Fatal(diags.Err()) 593 } 594 boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"} 595 beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"} 596 derp := addrs.Resource{Mode: addrs.DataResourceMode, Type: "test_source", Name: "derp"} 597 598 plan := &plans.Plan{ 599 Changes: &plans.Changes{ 600 Resources: []*plans.ResourceInstanceChangeSrc{ 601 { 602 Addr: boop.Instance(addrs.IntKey(0)).Absolute(root), 603 PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(root), 604 ChangeSrc: plans.ChangeSrc{Action: plans.CreateThenDelete}, 605 }, 606 { 607 Addr: boop.Instance(addrs.IntKey(1)).Absolute(root), 608 PrevRunAddr: boop.Instance(addrs.IntKey(1)).Absolute(root), 609 ChangeSrc: plans.ChangeSrc{Action: plans.Create}, 610 }, 611 { 612 Addr: boop.Instance(addrs.IntKey(0)).Absolute(vpc), 613 PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(vpc), 614 ChangeSrc: plans.ChangeSrc{Action: plans.Delete}, 615 }, 616 { 617 Addr: beep.Instance(addrs.NoKey).Absolute(root), 618 PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root), 619 ChangeSrc: plans.ChangeSrc{Action: plans.DeleteThenCreate}, 620 }, 621 { 622 Addr: beep.Instance(addrs.NoKey).Absolute(vpc), 623 PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(vpc), 624 ChangeSrc: plans.ChangeSrc{Action: plans.Update}, 625 }, 626 // Data source deletion should not show up in the logs 627 { 628 Addr: derp.Instance(addrs.NoKey).Absolute(root), 629 PrevRunAddr: derp.Instance(addrs.NoKey).Absolute(root), 630 ChangeSrc: plans.ChangeSrc{Action: plans.Delete}, 631 }, 632 }, 633 }, 634 } 635 v.Plan(plan, testSchemas()) 636 637 want := []map[string]interface{}{ 638 // Create-then-delete should result in replace 639 { 640 "@level": "info", 641 "@message": "test_resource.boop[0]: Plan to replace", 642 "@module": "terraform.ui", 643 "type": "planned_change", 644 "change": map[string]interface{}{ 645 "action": "replace", 646 "resource": map[string]interface{}{ 647 "addr": `test_resource.boop[0]`, 648 "implied_provider": "test", 649 "module": "", 650 "resource": `test_resource.boop[0]`, 651 "resource_key": float64(0), 652 "resource_name": "boop", 653 "resource_type": "test_resource", 654 }, 655 }, 656 }, 657 // Simple create 658 { 659 "@level": "info", 660 "@message": "test_resource.boop[1]: Plan to create", 661 "@module": "terraform.ui", 662 "type": "planned_change", 663 "change": map[string]interface{}{ 664 "action": "create", 665 "resource": map[string]interface{}{ 666 "addr": `test_resource.boop[1]`, 667 "implied_provider": "test", 668 "module": "", 669 "resource": `test_resource.boop[1]`, 670 "resource_key": float64(1), 671 "resource_name": "boop", 672 "resource_type": "test_resource", 673 }, 674 }, 675 }, 676 // Simple delete 677 { 678 "@level": "info", 679 "@message": "module.vpc.test_resource.boop[0]: Plan to delete", 680 "@module": "terraform.ui", 681 "type": "planned_change", 682 "change": map[string]interface{}{ 683 "action": "delete", 684 "resource": map[string]interface{}{ 685 "addr": `module.vpc.test_resource.boop[0]`, 686 "implied_provider": "test", 687 "module": "module.vpc", 688 "resource": `test_resource.boop[0]`, 689 "resource_key": float64(0), 690 "resource_name": "boop", 691 "resource_type": "test_resource", 692 }, 693 }, 694 }, 695 // Delete-then-create is also a replace 696 { 697 "@level": "info", 698 "@message": "test_resource.beep: Plan to replace", 699 "@module": "terraform.ui", 700 "type": "planned_change", 701 "change": map[string]interface{}{ 702 "action": "replace", 703 "resource": map[string]interface{}{ 704 "addr": `test_resource.beep`, 705 "implied_provider": "test", 706 "module": "", 707 "resource": `test_resource.beep`, 708 "resource_key": nil, 709 "resource_name": "beep", 710 "resource_type": "test_resource", 711 }, 712 }, 713 }, 714 // Simple update 715 { 716 "@level": "info", 717 "@message": "module.vpc.test_resource.beep: Plan to update", 718 "@module": "terraform.ui", 719 "type": "planned_change", 720 "change": map[string]interface{}{ 721 "action": "update", 722 "resource": map[string]interface{}{ 723 "addr": `module.vpc.test_resource.beep`, 724 "implied_provider": "test", 725 "module": "module.vpc", 726 "resource": `test_resource.beep`, 727 "resource_key": nil, 728 "resource_name": "beep", 729 "resource_type": "test_resource", 730 }, 731 }, 732 }, 733 // These counts are 3 add/1 change/3 destroy because the replace 734 // changes result in both add and destroy counts. 735 { 736 "@level": "info", 737 "@message": "Plan: 3 to add, 1 to change, 3 to destroy.", 738 "@module": "terraform.ui", 739 "type": "change_summary", 740 "changes": map[string]interface{}{ 741 "operation": "plan", 742 "add": float64(3), 743 "change": float64(1), 744 "remove": float64(3), 745 }, 746 }, 747 } 748 749 testJSONViewOutputEquals(t, done(t).Stdout(), want) 750 } 751 752 func TestOperationJSON_planDriftWithMove(t *testing.T) { 753 streams, done := terminal.StreamsForTesting(t) 754 v := &OperationJSON{view: NewJSONView(NewView(streams))} 755 756 root := addrs.RootModuleInstance 757 boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"} 758 beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"} 759 blep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "blep"} 760 honk := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "honk"} 761 762 plan := &plans.Plan{ 763 UIMode: plans.NormalMode, 764 Changes: &plans.Changes{ 765 Resources: []*plans.ResourceInstanceChangeSrc{ 766 { 767 Addr: honk.Instance(addrs.StringKey("bonk")).Absolute(root), 768 PrevRunAddr: honk.Instance(addrs.IntKey(0)).Absolute(root), 769 ChangeSrc: plans.ChangeSrc{Action: plans.NoOp}, 770 }, 771 }, 772 }, 773 DriftedResources: []*plans.ResourceInstanceChangeSrc{ 774 { 775 Addr: beep.Instance(addrs.NoKey).Absolute(root), 776 PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root), 777 ChangeSrc: plans.ChangeSrc{Action: plans.Delete}, 778 }, 779 { 780 Addr: boop.Instance(addrs.NoKey).Absolute(root), 781 PrevRunAddr: blep.Instance(addrs.NoKey).Absolute(root), 782 ChangeSrc: plans.ChangeSrc{Action: plans.Update}, 783 }, 784 // Move-only resource drift should not be present in normal mode plans 785 { 786 Addr: honk.Instance(addrs.StringKey("bonk")).Absolute(root), 787 PrevRunAddr: honk.Instance(addrs.IntKey(0)).Absolute(root), 788 ChangeSrc: plans.ChangeSrc{Action: plans.NoOp}, 789 }, 790 }, 791 } 792 v.Plan(plan, testSchemas()) 793 794 want := []map[string]interface{}{ 795 // Drift detected: delete 796 { 797 "@level": "info", 798 "@message": "test_resource.beep: Drift detected (delete)", 799 "@module": "terraform.ui", 800 "type": "resource_drift", 801 "change": map[string]interface{}{ 802 "action": "delete", 803 "resource": map[string]interface{}{ 804 "addr": "test_resource.beep", 805 "implied_provider": "test", 806 "module": "", 807 "resource": "test_resource.beep", 808 "resource_key": nil, 809 "resource_name": "beep", 810 "resource_type": "test_resource", 811 }, 812 }, 813 }, 814 // Drift detected: update with move 815 { 816 "@level": "info", 817 "@message": "test_resource.boop: Drift detected (update)", 818 "@module": "terraform.ui", 819 "type": "resource_drift", 820 "change": map[string]interface{}{ 821 "action": "update", 822 "resource": map[string]interface{}{ 823 "addr": "test_resource.boop", 824 "implied_provider": "test", 825 "module": "", 826 "resource": "test_resource.boop", 827 "resource_key": nil, 828 "resource_name": "boop", 829 "resource_type": "test_resource", 830 }, 831 "previous_resource": map[string]interface{}{ 832 "addr": "test_resource.blep", 833 "implied_provider": "test", 834 "module": "", 835 "resource": "test_resource.blep", 836 "resource_key": nil, 837 "resource_name": "blep", 838 "resource_type": "test_resource", 839 }, 840 }, 841 }, 842 // Move-only change 843 { 844 "@level": "info", 845 "@message": `test_resource.honk["bonk"]: Plan to move`, 846 "@module": "terraform.ui", 847 "type": "planned_change", 848 "change": map[string]interface{}{ 849 "action": "move", 850 "resource": map[string]interface{}{ 851 "addr": `test_resource.honk["bonk"]`, 852 "implied_provider": "test", 853 "module": "", 854 "resource": `test_resource.honk["bonk"]`, 855 "resource_key": "bonk", 856 "resource_name": "honk", 857 "resource_type": "test_resource", 858 }, 859 "previous_resource": map[string]interface{}{ 860 "addr": `test_resource.honk[0]`, 861 "implied_provider": "test", 862 "module": "", 863 "resource": `test_resource.honk[0]`, 864 "resource_key": float64(0), 865 "resource_name": "honk", 866 "resource_type": "test_resource", 867 }, 868 }, 869 }, 870 // No changes 871 { 872 "@level": "info", 873 "@message": "Plan: 0 to add, 0 to change, 0 to destroy.", 874 "@module": "terraform.ui", 875 "type": "change_summary", 876 "changes": map[string]interface{}{ 877 "operation": "plan", 878 "add": float64(0), 879 "change": float64(0), 880 "remove": float64(0), 881 }, 882 }, 883 } 884 885 testJSONViewOutputEquals(t, done(t).Stdout(), want) 886 } 887 888 func TestOperationJSON_planDriftWithMoveRefreshOnly(t *testing.T) { 889 streams, done := terminal.StreamsForTesting(t) 890 v := &OperationJSON{view: NewJSONView(NewView(streams))} 891 892 root := addrs.RootModuleInstance 893 boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"} 894 beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"} 895 blep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "blep"} 896 honk := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "honk"} 897 898 plan := &plans.Plan{ 899 UIMode: plans.RefreshOnlyMode, 900 Changes: &plans.Changes{ 901 Resources: []*plans.ResourceInstanceChangeSrc{}, 902 }, 903 DriftedResources: []*plans.ResourceInstanceChangeSrc{ 904 { 905 Addr: beep.Instance(addrs.NoKey).Absolute(root), 906 PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root), 907 ChangeSrc: plans.ChangeSrc{Action: plans.Delete}, 908 }, 909 { 910 Addr: boop.Instance(addrs.NoKey).Absolute(root), 911 PrevRunAddr: blep.Instance(addrs.NoKey).Absolute(root), 912 ChangeSrc: plans.ChangeSrc{Action: plans.Update}, 913 }, 914 // Move-only resource drift should be present in refresh-only plans 915 { 916 Addr: honk.Instance(addrs.StringKey("bonk")).Absolute(root), 917 PrevRunAddr: honk.Instance(addrs.IntKey(0)).Absolute(root), 918 ChangeSrc: plans.ChangeSrc{Action: plans.NoOp}, 919 }, 920 }, 921 } 922 v.Plan(plan, testSchemas()) 923 924 want := []map[string]interface{}{ 925 // Drift detected: delete 926 { 927 "@level": "info", 928 "@message": "test_resource.beep: Drift detected (delete)", 929 "@module": "terraform.ui", 930 "type": "resource_drift", 931 "change": map[string]interface{}{ 932 "action": "delete", 933 "resource": map[string]interface{}{ 934 "addr": "test_resource.beep", 935 "implied_provider": "test", 936 "module": "", 937 "resource": "test_resource.beep", 938 "resource_key": nil, 939 "resource_name": "beep", 940 "resource_type": "test_resource", 941 }, 942 }, 943 }, 944 // Drift detected: update 945 { 946 "@level": "info", 947 "@message": "test_resource.boop: Drift detected (update)", 948 "@module": "terraform.ui", 949 "type": "resource_drift", 950 "change": map[string]interface{}{ 951 "action": "update", 952 "resource": map[string]interface{}{ 953 "addr": "test_resource.boop", 954 "implied_provider": "test", 955 "module": "", 956 "resource": "test_resource.boop", 957 "resource_key": nil, 958 "resource_name": "boop", 959 "resource_type": "test_resource", 960 }, 961 "previous_resource": map[string]interface{}{ 962 "addr": "test_resource.blep", 963 "implied_provider": "test", 964 "module": "", 965 "resource": "test_resource.blep", 966 "resource_key": nil, 967 "resource_name": "blep", 968 "resource_type": "test_resource", 969 }, 970 }, 971 }, 972 // Drift detected: Move-only change 973 { 974 "@level": "info", 975 "@message": `test_resource.honk["bonk"]: Drift detected (move)`, 976 "@module": "terraform.ui", 977 "type": "resource_drift", 978 "change": map[string]interface{}{ 979 "action": "move", 980 "resource": map[string]interface{}{ 981 "addr": `test_resource.honk["bonk"]`, 982 "implied_provider": "test", 983 "module": "", 984 "resource": `test_resource.honk["bonk"]`, 985 "resource_key": "bonk", 986 "resource_name": "honk", 987 "resource_type": "test_resource", 988 }, 989 "previous_resource": map[string]interface{}{ 990 "addr": `test_resource.honk[0]`, 991 "implied_provider": "test", 992 "module": "", 993 "resource": `test_resource.honk[0]`, 994 "resource_key": float64(0), 995 "resource_name": "honk", 996 "resource_type": "test_resource", 997 }, 998 }, 999 }, 1000 // No changes 1001 { 1002 "@level": "info", 1003 "@message": "Plan: 0 to add, 0 to change, 0 to destroy.", 1004 "@module": "terraform.ui", 1005 "type": "change_summary", 1006 "changes": map[string]interface{}{ 1007 "operation": "plan", 1008 "add": float64(0), 1009 "change": float64(0), 1010 "remove": float64(0), 1011 }, 1012 }, 1013 } 1014 1015 testJSONViewOutputEquals(t, done(t).Stdout(), want) 1016 } 1017 1018 func TestOperationJSON_planOutputChanges(t *testing.T) { 1019 streams, done := terminal.StreamsForTesting(t) 1020 v := &OperationJSON{view: NewJSONView(NewView(streams))} 1021 1022 root := addrs.RootModuleInstance 1023 1024 plan := &plans.Plan{ 1025 Changes: &plans.Changes{ 1026 Resources: []*plans.ResourceInstanceChangeSrc{}, 1027 Outputs: []*plans.OutputChangeSrc{ 1028 { 1029 Addr: root.OutputValue("boop"), 1030 ChangeSrc: plans.ChangeSrc{ 1031 Action: plans.NoOp, 1032 }, 1033 }, 1034 { 1035 Addr: root.OutputValue("beep"), 1036 ChangeSrc: plans.ChangeSrc{ 1037 Action: plans.Create, 1038 }, 1039 }, 1040 { 1041 Addr: root.OutputValue("bonk"), 1042 ChangeSrc: plans.ChangeSrc{ 1043 Action: plans.Delete, 1044 }, 1045 }, 1046 { 1047 Addr: root.OutputValue("honk"), 1048 ChangeSrc: plans.ChangeSrc{ 1049 Action: plans.Update, 1050 }, 1051 Sensitive: true, 1052 }, 1053 }, 1054 }, 1055 } 1056 v.Plan(plan, testSchemas()) 1057 1058 want := []map[string]interface{}{ 1059 // No resource changes 1060 { 1061 "@level": "info", 1062 "@message": "Plan: 0 to add, 0 to change, 0 to destroy.", 1063 "@module": "terraform.ui", 1064 "type": "change_summary", 1065 "changes": map[string]interface{}{ 1066 "operation": "plan", 1067 "add": float64(0), 1068 "change": float64(0), 1069 "remove": float64(0), 1070 }, 1071 }, 1072 // Output changes 1073 { 1074 "@level": "info", 1075 "@message": "Outputs: 4", 1076 "@module": "terraform.ui", 1077 "type": "outputs", 1078 "outputs": map[string]interface{}{ 1079 "boop": map[string]interface{}{ 1080 "action": "noop", 1081 "sensitive": false, 1082 }, 1083 "beep": map[string]interface{}{ 1084 "action": "create", 1085 "sensitive": false, 1086 }, 1087 "bonk": map[string]interface{}{ 1088 "action": "delete", 1089 "sensitive": false, 1090 }, 1091 "honk": map[string]interface{}{ 1092 "action": "update", 1093 "sensitive": true, 1094 }, 1095 }, 1096 }, 1097 } 1098 1099 testJSONViewOutputEquals(t, done(t).Stdout(), want) 1100 } 1101 1102 func TestOperationJSON_plannedChange(t *testing.T) { 1103 streams, done := terminal.StreamsForTesting(t) 1104 v := &OperationJSON{view: NewJSONView(NewView(streams))} 1105 1106 root := addrs.RootModuleInstance 1107 boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "boop"} 1108 derp := addrs.Resource{Mode: addrs.DataResourceMode, Type: "test_source", Name: "derp"} 1109 1110 // Replace requested by user 1111 v.PlannedChange(&plans.ResourceInstanceChangeSrc{ 1112 Addr: boop.Instance(addrs.IntKey(0)).Absolute(root), 1113 PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(root), 1114 ChangeSrc: plans.ChangeSrc{Action: plans.DeleteThenCreate}, 1115 ActionReason: plans.ResourceInstanceReplaceByRequest, 1116 }) 1117 1118 // Simple create 1119 v.PlannedChange(&plans.ResourceInstanceChangeSrc{ 1120 Addr: boop.Instance(addrs.IntKey(1)).Absolute(root), 1121 PrevRunAddr: boop.Instance(addrs.IntKey(1)).Absolute(root), 1122 ChangeSrc: plans.ChangeSrc{Action: plans.Create}, 1123 }) 1124 1125 // Data source deletion 1126 v.PlannedChange(&plans.ResourceInstanceChangeSrc{ 1127 Addr: derp.Instance(addrs.NoKey).Absolute(root), 1128 PrevRunAddr: derp.Instance(addrs.NoKey).Absolute(root), 1129 ChangeSrc: plans.ChangeSrc{Action: plans.Delete}, 1130 }) 1131 1132 // Expect only two messages, as the data source deletion should be a no-op 1133 want := []map[string]interface{}{ 1134 { 1135 "@level": "info", 1136 "@message": "test_instance.boop[0]: Plan to replace", 1137 "@module": "terraform.ui", 1138 "type": "planned_change", 1139 "change": map[string]interface{}{ 1140 "action": "replace", 1141 "reason": "requested", 1142 "resource": map[string]interface{}{ 1143 "addr": `test_instance.boop[0]`, 1144 "implied_provider": "test", 1145 "module": "", 1146 "resource": `test_instance.boop[0]`, 1147 "resource_key": float64(0), 1148 "resource_name": "boop", 1149 "resource_type": "test_instance", 1150 }, 1151 }, 1152 }, 1153 { 1154 "@level": "info", 1155 "@message": "test_instance.boop[1]: Plan to create", 1156 "@module": "terraform.ui", 1157 "type": "planned_change", 1158 "change": map[string]interface{}{ 1159 "action": "create", 1160 "resource": map[string]interface{}{ 1161 "addr": `test_instance.boop[1]`, 1162 "implied_provider": "test", 1163 "module": "", 1164 "resource": `test_instance.boop[1]`, 1165 "resource_key": float64(1), 1166 "resource_name": "boop", 1167 "resource_type": "test_instance", 1168 }, 1169 }, 1170 }, 1171 } 1172 1173 testJSONViewOutputEquals(t, done(t).Stdout(), want) 1174 }