github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/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/iaas-resource-provision/iaas-rpc/internal/addrs" 10 "github.com/iaas-resource-provision/iaas-rpc/internal/command/arguments" 11 "github.com/iaas-resource-provision/iaas-rpc/internal/plans" 12 "github.com/iaas-resource-provision/iaas-rpc/internal/states" 13 "github.com/iaas-resource-provision/iaas-rpc/internal/states/statefile" 14 "github.com/iaas-resource-provision/iaas-rpc/internal/terminal" 15 "github.com/iaas-resource-provision/iaas-rpc/internal/terraform" 16 ) 17 18 func TestOperation_stopping(t *testing.T) { 19 streams, done := terminal.StreamsForTesting(t) 20 v := NewOperation(arguments.ViewHuman, false, NewView(streams)) 21 22 v.Stopping() 23 24 if got, want := done(t).Stdout(), "Stopping operation...\n"; got != want { 25 t.Errorf("wrong result\ngot: %q\nwant: %q", got, want) 26 } 27 } 28 29 func TestOperation_cancelled(t *testing.T) { 30 testCases := map[string]struct { 31 planMode plans.Mode 32 want string 33 }{ 34 "apply": { 35 planMode: plans.NormalMode, 36 want: "Apply cancelled.\n", 37 }, 38 "destroy": { 39 planMode: plans.DestroyMode, 40 want: "Destroy cancelled.\n", 41 }, 42 } 43 for name, tc := range testCases { 44 t.Run(name, func(t *testing.T) { 45 streams, done := terminal.StreamsForTesting(t) 46 v := NewOperation(arguments.ViewHuman, false, NewView(streams)) 47 48 v.Cancelled(tc.planMode) 49 50 if got, want := done(t).Stdout(), tc.want; got != want { 51 t.Errorf("wrong result\ngot: %q\nwant: %q", got, want) 52 } 53 }) 54 } 55 } 56 57 func TestOperation_emergencyDumpState(t *testing.T) { 58 streams, done := terminal.StreamsForTesting(t) 59 v := NewOperation(arguments.ViewHuman, false, NewView(streams)) 60 61 stateFile := statefile.New(nil, "foo", 1) 62 63 err := v.EmergencyDumpState(stateFile) 64 if err != nil { 65 t.Fatalf("unexpected error dumping state: %s", err) 66 } 67 68 // Check that the result (on stderr) looks like JSON state 69 raw := done(t).Stderr() 70 var state map[string]interface{} 71 if err := json.Unmarshal([]byte(raw), &state); err != nil { 72 t.Fatalf("unexpected error parsing dumped state: %s\nraw:\n%s", err, raw) 73 } 74 } 75 76 func TestOperation_planNoChanges(t *testing.T) { 77 78 tests := map[string]struct { 79 plan func(schemas *terraform.Schemas) *plans.Plan 80 wantText string 81 }{ 82 "nothing at all in normal mode": { 83 func(schemas *terraform.Schemas) *plans.Plan { 84 return &plans.Plan{ 85 UIMode: plans.NormalMode, 86 Changes: plans.NewChanges(), 87 PrevRunState: states.NewState(), 88 PriorState: states.NewState(), 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 PrevRunState: states.NewState(), 99 PriorState: states.NewState(), 100 } 101 }, 102 "Terraform has checked that the real remote objects still match", 103 }, 104 "nothing at all in destroy mode": { 105 func(schemas *terraform.Schemas) *plans.Plan { 106 return &plans.Plan{ 107 UIMode: plans.DestroyMode, 108 Changes: plans.NewChanges(), 109 PrevRunState: states.NewState(), 110 PriorState: states.NewState(), 111 } 112 }, 113 "No objects need to be destroyed.", 114 }, 115 "no drift to display with only deposed instances": { 116 // changes in deposed instances will cause a change in state, but 117 // have nothing to display to the user 118 func(schemas *terraform.Schemas) *plans.Plan { 119 return &plans.Plan{ 120 UIMode: plans.NormalMode, 121 Changes: plans.NewChanges(), 122 PrevRunState: states.BuildState(func(state *states.SyncState) { 123 state.SetResourceInstanceDeposed( 124 addrs.Resource{ 125 Mode: addrs.ManagedResourceMode, 126 Type: "test_resource", 127 Name: "somewhere", 128 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 129 states.NewDeposedKey(), 130 &states.ResourceInstanceObjectSrc{ 131 Status: states.ObjectReady, 132 AttrsJSON: []byte(`{"foo": "ok", "bars":[]}`), 133 }, 134 addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 135 ) 136 }), 137 PriorState: states.NewState(), 138 } 139 }, 140 "no differences, so no changes are needed.", 141 }, 142 "drift detected in normal mode": { 143 func(schemas *terraform.Schemas) *plans.Plan { 144 return &plans.Plan{ 145 UIMode: plans.NormalMode, 146 Changes: plans.NewChanges(), 147 PrevRunState: states.BuildState(func(state *states.SyncState) { 148 state.SetResourceInstanceCurrent( 149 addrs.Resource{ 150 Mode: addrs.ManagedResourceMode, 151 Type: "test_resource", 152 Name: "somewhere", 153 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 154 &states.ResourceInstanceObjectSrc{ 155 Status: states.ObjectReady, 156 AttrsJSON: []byte(`{}`), 157 }, 158 addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 159 ) 160 }), 161 PriorState: states.NewState(), 162 } 163 }, 164 "to update the Terraform state to match, create and apply a refresh-only plan", 165 }, 166 "drift detected with deposed": { 167 func(schemas *terraform.Schemas) *plans.Plan { 168 return &plans.Plan{ 169 UIMode: plans.NormalMode, 170 Changes: plans.NewChanges(), 171 PrevRunState: states.BuildState(func(state *states.SyncState) { 172 state.SetResourceInstanceCurrent( 173 addrs.Resource{ 174 Mode: addrs.ManagedResourceMode, 175 Type: "test_resource", 176 Name: "changes", 177 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 178 &states.ResourceInstanceObjectSrc{ 179 Status: states.ObjectReady, 180 AttrsJSON: []byte(`{"foo":"b"}`), 181 }, 182 addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 183 ) 184 state.SetResourceInstanceDeposed( 185 addrs.Resource{ 186 Mode: addrs.ManagedResourceMode, 187 Type: "test_resource", 188 Name: "broken", 189 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 190 states.NewDeposedKey(), 191 &states.ResourceInstanceObjectSrc{ 192 Status: states.ObjectReady, 193 AttrsJSON: []byte(`{"foo":"c"}`), 194 }, 195 addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 196 ) 197 }), 198 PriorState: states.BuildState(func(state *states.SyncState) { 199 state.SetResourceInstanceCurrent( 200 addrs.Resource{ 201 Mode: addrs.ManagedResourceMode, 202 Type: "test_resource", 203 Name: "changed", 204 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 205 &states.ResourceInstanceObjectSrc{ 206 Status: states.ObjectReady, 207 AttrsJSON: []byte(`{"foo":"b"}`), 208 }, 209 addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 210 ) 211 state.SetResourceInstanceDeposed( 212 addrs.Resource{ 213 Mode: addrs.ManagedResourceMode, 214 Type: "test_resource", 215 Name: "broken", 216 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 217 states.NewDeposedKey(), 218 &states.ResourceInstanceObjectSrc{ 219 Status: states.ObjectReady, 220 AttrsJSON: []byte(`{"foo":"d"}`), 221 }, 222 addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 223 ) 224 }), 225 } 226 }, 227 "to update the Terraform state to match, create and apply a refresh-only plan", 228 }, 229 "drift detected in refresh-only mode": { 230 func(schemas *terraform.Schemas) *plans.Plan { 231 return &plans.Plan{ 232 UIMode: plans.RefreshOnlyMode, 233 Changes: plans.NewChanges(), 234 PrevRunState: states.BuildState(func(state *states.SyncState) { 235 state.SetResourceInstanceCurrent( 236 addrs.Resource{ 237 Mode: addrs.ManagedResourceMode, 238 Type: "test_resource", 239 Name: "somewhere", 240 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 241 &states.ResourceInstanceObjectSrc{ 242 Status: states.ObjectReady, 243 AttrsJSON: []byte(`{}`), 244 }, 245 addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 246 ) 247 }), 248 PriorState: states.NewState(), 249 } 250 }, 251 "If you were expecting these changes then you can apply this plan", 252 }, 253 "drift detected in destroy mode": { 254 func(schemas *terraform.Schemas) *plans.Plan { 255 return &plans.Plan{ 256 UIMode: plans.DestroyMode, 257 Changes: plans.NewChanges(), 258 PrevRunState: states.BuildState(func(state *states.SyncState) { 259 state.SetResourceInstanceCurrent( 260 addrs.Resource{ 261 Mode: addrs.ManagedResourceMode, 262 Type: "test_resource", 263 Name: "somewhere", 264 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 265 &states.ResourceInstanceObjectSrc{ 266 Status: states.ObjectReady, 267 AttrsJSON: []byte(`{}`), 268 }, 269 addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 270 ) 271 }), 272 PriorState: states.NewState(), 273 } 274 }, 275 "No objects need to be destroyed.", 276 }, 277 } 278 279 schemas := testSchemas() 280 for name, test := range tests { 281 t.Run(name, func(t *testing.T) { 282 streams, done := terminal.StreamsForTesting(t) 283 v := NewOperation(arguments.ViewHuman, false, NewView(streams)) 284 plan := test.plan(schemas) 285 v.Plan(plan, schemas) 286 got := done(t).Stdout() 287 if want := test.wantText; want != "" && !strings.Contains(got, want) { 288 t.Errorf("missing expected message\ngot:\n%s\n\nwant substring: %s", got, want) 289 } 290 }) 291 } 292 } 293 294 func TestOperation_plan(t *testing.T) { 295 streams, done := terminal.StreamsForTesting(t) 296 v := NewOperation(arguments.ViewHuman, true, NewView(streams)) 297 298 plan := testPlan(t) 299 schemas := testSchemas() 300 v.Plan(plan, schemas) 301 302 want := ` 303 RPC (Resource Provision Center) used the selected providers to generate the following execution 304 plan. Resource actions are indicated with the following symbols: 305 + create 306 307 RPC (Resource Provision Center) will perform the following actions: 308 309 # test_resource.foo will be created 310 + resource "test_resource" "foo" { 311 + foo = "bar" 312 + id = (known after apply) 313 } 314 315 Plan: 1 to add, 0 to change, 0 to destroy. 316 ` 317 318 if got := done(t).Stdout(); got != want { 319 t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s", got, want) 320 } 321 } 322 323 func TestOperation_planNextStep(t *testing.T) { 324 testCases := map[string]struct { 325 path string 326 want string 327 }{ 328 "no state path": { 329 path: "", 330 want: "You didn't use the -out option", 331 }, 332 "state path": { 333 path: "good plan.tfplan", 334 want: `terraform apply "good plan.tfplan"`, 335 }, 336 } 337 for name, tc := range testCases { 338 t.Run(name, func(t *testing.T) { 339 streams, done := terminal.StreamsForTesting(t) 340 v := NewOperation(arguments.ViewHuman, false, NewView(streams)) 341 342 v.PlanNextStep(tc.path) 343 344 if got := done(t).Stdout(); !strings.Contains(got, tc.want) { 345 t.Errorf("wrong result\ngot: %q\nwant: %q", got, tc.want) 346 } 347 }) 348 } 349 } 350 351 // The in-automation state is on the view itself, so testing it separately is 352 // clearer. 353 func TestOperation_planNextStepInAutomation(t *testing.T) { 354 streams, done := terminal.StreamsForTesting(t) 355 v := NewOperation(arguments.ViewHuman, true, NewView(streams)) 356 357 v.PlanNextStep("") 358 359 if got := done(t).Stdout(); got != "" { 360 t.Errorf("unexpected output\ngot: %q", got) 361 } 362 } 363 364 // Test all the trivial OperationJSON methods together. Y'know, for brevity. 365 // This test is not a realistic stream of messages. 366 func TestOperationJSON_logs(t *testing.T) { 367 streams, done := terminal.StreamsForTesting(t) 368 v := &OperationJSON{view: NewJSONView(NewView(streams))} 369 370 v.Cancelled(plans.NormalMode) 371 v.Cancelled(plans.DestroyMode) 372 v.Stopping() 373 v.Interrupted() 374 v.FatalInterrupt() 375 376 want := []map[string]interface{}{ 377 { 378 "@level": "info", 379 "@message": "Apply cancelled", 380 "@module": "terraform.ui", 381 "type": "log", 382 }, 383 { 384 "@level": "info", 385 "@message": "Destroy cancelled", 386 "@module": "terraform.ui", 387 "type": "log", 388 }, 389 { 390 "@level": "info", 391 "@message": "Stopping operation...", 392 "@module": "terraform.ui", 393 "type": "log", 394 }, 395 { 396 "@level": "info", 397 "@message": interrupted, 398 "@module": "terraform.ui", 399 "type": "log", 400 }, 401 { 402 "@level": "info", 403 "@message": fatalInterrupt, 404 "@module": "terraform.ui", 405 "type": "log", 406 }, 407 } 408 409 testJSONViewOutputEquals(t, done(t).Stdout(), want) 410 } 411 412 // This is a fairly circular test, but it's such a rarely executed code path 413 // that I think it's probably still worth having. We're not testing against 414 // a fixed state JSON output because this test ought not fail just because 415 // we upgrade state format in the future. 416 func TestOperationJSON_emergencyDumpState(t *testing.T) { 417 streams, done := terminal.StreamsForTesting(t) 418 v := &OperationJSON{view: NewJSONView(NewView(streams))} 419 420 stateFile := statefile.New(nil, "foo", 1) 421 stateBuf := new(bytes.Buffer) 422 err := statefile.Write(stateFile, stateBuf) 423 if err != nil { 424 t.Fatal(err) 425 } 426 var stateJSON map[string]interface{} 427 err = json.Unmarshal(stateBuf.Bytes(), &stateJSON) 428 if err != nil { 429 t.Fatal(err) 430 } 431 432 err = v.EmergencyDumpState(stateFile) 433 if err != nil { 434 t.Fatalf("unexpected error dumping state: %s", err) 435 } 436 437 want := []map[string]interface{}{ 438 { 439 "@level": "info", 440 "@message": "Emergency state dump", 441 "@module": "terraform.ui", 442 "type": "log", 443 "state": stateJSON, 444 }, 445 } 446 447 testJSONViewOutputEquals(t, done(t).Stdout(), want) 448 } 449 450 func TestOperationJSON_planNoChanges(t *testing.T) { 451 streams, done := terminal.StreamsForTesting(t) 452 v := &OperationJSON{view: NewJSONView(NewView(streams))} 453 454 plan := &plans.Plan{ 455 Changes: plans.NewChanges(), 456 } 457 v.Plan(plan, nil) 458 459 want := []map[string]interface{}{ 460 { 461 "@level": "info", 462 "@message": "Plan: 0 to add, 0 to change, 0 to destroy.", 463 "@module": "terraform.ui", 464 "type": "change_summary", 465 "changes": map[string]interface{}{ 466 "operation": "plan", 467 "add": float64(0), 468 "change": float64(0), 469 "remove": float64(0), 470 }, 471 }, 472 } 473 474 testJSONViewOutputEquals(t, done(t).Stdout(), want) 475 } 476 477 func TestOperationJSON_plan(t *testing.T) { 478 streams, done := terminal.StreamsForTesting(t) 479 v := &OperationJSON{view: NewJSONView(NewView(streams))} 480 481 root := addrs.RootModuleInstance 482 vpc, diags := addrs.ParseModuleInstanceStr("module.vpc") 483 if len(diags) > 0 { 484 t.Fatal(diags.Err()) 485 } 486 boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"} 487 beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"} 488 derp := addrs.Resource{Mode: addrs.DataResourceMode, Type: "test_source", Name: "derp"} 489 490 plan := &plans.Plan{ 491 Changes: &plans.Changes{ 492 Resources: []*plans.ResourceInstanceChangeSrc{ 493 { 494 Addr: boop.Instance(addrs.IntKey(0)).Absolute(root), 495 ChangeSrc: plans.ChangeSrc{Action: plans.CreateThenDelete}, 496 }, 497 { 498 Addr: boop.Instance(addrs.IntKey(1)).Absolute(root), 499 ChangeSrc: plans.ChangeSrc{Action: plans.Create}, 500 }, 501 { 502 Addr: boop.Instance(addrs.IntKey(0)).Absolute(vpc), 503 ChangeSrc: plans.ChangeSrc{Action: plans.Delete}, 504 }, 505 { 506 Addr: beep.Instance(addrs.NoKey).Absolute(root), 507 ChangeSrc: plans.ChangeSrc{Action: plans.DeleteThenCreate}, 508 }, 509 { 510 Addr: beep.Instance(addrs.NoKey).Absolute(vpc), 511 ChangeSrc: plans.ChangeSrc{Action: plans.Update}, 512 }, 513 // Data source deletion should not show up in the logs 514 { 515 Addr: derp.Instance(addrs.NoKey).Absolute(root), 516 ChangeSrc: plans.ChangeSrc{Action: plans.Delete}, 517 }, 518 }, 519 }, 520 } 521 v.Plan(plan, testSchemas()) 522 523 want := []map[string]interface{}{ 524 // Create-then-delete should result in replace 525 { 526 "@level": "info", 527 "@message": "test_resource.boop[0]: Plan to replace", 528 "@module": "terraform.ui", 529 "type": "planned_change", 530 "change": map[string]interface{}{ 531 "action": "replace", 532 "resource": map[string]interface{}{ 533 "addr": `test_resource.boop[0]`, 534 "implied_provider": "test", 535 "module": "", 536 "resource": `test_resource.boop[0]`, 537 "resource_key": float64(0), 538 "resource_name": "boop", 539 "resource_type": "test_resource", 540 }, 541 }, 542 }, 543 // Simple create 544 { 545 "@level": "info", 546 "@message": "test_resource.boop[1]: Plan to create", 547 "@module": "terraform.ui", 548 "type": "planned_change", 549 "change": map[string]interface{}{ 550 "action": "create", 551 "resource": map[string]interface{}{ 552 "addr": `test_resource.boop[1]`, 553 "implied_provider": "test", 554 "module": "", 555 "resource": `test_resource.boop[1]`, 556 "resource_key": float64(1), 557 "resource_name": "boop", 558 "resource_type": "test_resource", 559 }, 560 }, 561 }, 562 // Simple delete 563 { 564 "@level": "info", 565 "@message": "module.vpc.test_resource.boop[0]: Plan to delete", 566 "@module": "terraform.ui", 567 "type": "planned_change", 568 "change": map[string]interface{}{ 569 "action": "delete", 570 "resource": map[string]interface{}{ 571 "addr": `module.vpc.test_resource.boop[0]`, 572 "implied_provider": "test", 573 "module": "module.vpc", 574 "resource": `test_resource.boop[0]`, 575 "resource_key": float64(0), 576 "resource_name": "boop", 577 "resource_type": "test_resource", 578 }, 579 }, 580 }, 581 // Delete-then-create is also a replace 582 { 583 "@level": "info", 584 "@message": "test_resource.beep: Plan to replace", 585 "@module": "terraform.ui", 586 "type": "planned_change", 587 "change": map[string]interface{}{ 588 "action": "replace", 589 "resource": map[string]interface{}{ 590 "addr": `test_resource.beep`, 591 "implied_provider": "test", 592 "module": "", 593 "resource": `test_resource.beep`, 594 "resource_key": nil, 595 "resource_name": "beep", 596 "resource_type": "test_resource", 597 }, 598 }, 599 }, 600 // Simple update 601 { 602 "@level": "info", 603 "@message": "module.vpc.test_resource.beep: Plan to update", 604 "@module": "terraform.ui", 605 "type": "planned_change", 606 "change": map[string]interface{}{ 607 "action": "update", 608 "resource": map[string]interface{}{ 609 "addr": `module.vpc.test_resource.beep`, 610 "implied_provider": "test", 611 "module": "module.vpc", 612 "resource": `test_resource.beep`, 613 "resource_key": nil, 614 "resource_name": "beep", 615 "resource_type": "test_resource", 616 }, 617 }, 618 }, 619 // These counts are 3 add/1 change/3 destroy because the replace 620 // changes result in both add and destroy counts. 621 { 622 "@level": "info", 623 "@message": "Plan: 3 to add, 1 to change, 3 to destroy.", 624 "@module": "terraform.ui", 625 "type": "change_summary", 626 "changes": map[string]interface{}{ 627 "operation": "plan", 628 "add": float64(3), 629 "change": float64(1), 630 "remove": float64(3), 631 }, 632 }, 633 } 634 635 testJSONViewOutputEquals(t, done(t).Stdout(), want) 636 } 637 638 func TestOperationJSON_planDrift(t *testing.T) { 639 streams, done := terminal.StreamsForTesting(t) 640 v := &OperationJSON{view: NewJSONView(NewView(streams))} 641 642 root := addrs.RootModuleInstance 643 boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"} 644 beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"} 645 derp := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "derp"} 646 647 plan := &plans.Plan{ 648 Changes: &plans.Changes{ 649 Resources: []*plans.ResourceInstanceChangeSrc{}, 650 }, 651 PrevRunState: states.BuildState(func(state *states.SyncState) { 652 // Update 653 state.SetResourceInstanceCurrent( 654 boop.Instance(addrs.NoKey).Absolute(root), 655 &states.ResourceInstanceObjectSrc{ 656 Status: states.ObjectReady, 657 AttrsJSON: []byte(`{"foo":"bar"}`), 658 }, 659 root.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 660 ) 661 // Delete 662 state.SetResourceInstanceCurrent( 663 beep.Instance(addrs.NoKey).Absolute(root), 664 &states.ResourceInstanceObjectSrc{ 665 Status: states.ObjectReady, 666 AttrsJSON: []byte(`{"foo":"boop"}`), 667 }, 668 root.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 669 ) 670 // No-op 671 state.SetResourceInstanceCurrent( 672 derp.Instance(addrs.NoKey).Absolute(root), 673 &states.ResourceInstanceObjectSrc{ 674 Status: states.ObjectReady, 675 AttrsJSON: []byte(`{"foo":"boop"}`), 676 }, 677 root.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 678 ) 679 }), 680 PriorState: states.BuildState(func(state *states.SyncState) { 681 // Update 682 state.SetResourceInstanceCurrent( 683 boop.Instance(addrs.NoKey).Absolute(root), 684 &states.ResourceInstanceObjectSrc{ 685 Status: states.ObjectReady, 686 AttrsJSON: []byte(`{"foo":"baz"}`), 687 }, 688 root.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 689 ) 690 // Delete 691 state.SetResourceInstanceCurrent( 692 beep.Instance(addrs.NoKey).Absolute(root), 693 nil, 694 root.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 695 ) 696 // No-op 697 state.SetResourceInstanceCurrent( 698 derp.Instance(addrs.NoKey).Absolute(root), 699 &states.ResourceInstanceObjectSrc{ 700 Status: states.ObjectReady, 701 AttrsJSON: []byte(`{"foo":"boop"}`), 702 }, 703 root.ProviderConfigDefault(addrs.NewDefaultProvider("test")), 704 ) 705 }), 706 } 707 v.Plan(plan, testSchemas()) 708 709 want := []map[string]interface{}{ 710 // Drift detected: delete 711 { 712 "@level": "info", 713 "@message": "test_resource.beep: Drift detected (delete)", 714 "@module": "terraform.ui", 715 "type": "resource_drift", 716 "change": map[string]interface{}{ 717 "action": "delete", 718 "resource": map[string]interface{}{ 719 "addr": "test_resource.beep", 720 "implied_provider": "test", 721 "module": "", 722 "resource": "test_resource.beep", 723 "resource_key": nil, 724 "resource_name": "beep", 725 "resource_type": "test_resource", 726 }, 727 }, 728 }, 729 // Drift detected: update 730 { 731 "@level": "info", 732 "@message": "test_resource.boop: Drift detected (update)", 733 "@module": "terraform.ui", 734 "type": "resource_drift", 735 "change": map[string]interface{}{ 736 "action": "update", 737 "resource": map[string]interface{}{ 738 "addr": "test_resource.boop", 739 "implied_provider": "test", 740 "module": "", 741 "resource": "test_resource.boop", 742 "resource_key": nil, 743 "resource_name": "boop", 744 "resource_type": "test_resource", 745 }, 746 }, 747 }, 748 // No changes 749 { 750 "@level": "info", 751 "@message": "Plan: 0 to add, 0 to change, 0 to destroy.", 752 "@module": "terraform.ui", 753 "type": "change_summary", 754 "changes": map[string]interface{}{ 755 "operation": "plan", 756 "add": float64(0), 757 "change": float64(0), 758 "remove": float64(0), 759 }, 760 }, 761 } 762 763 testJSONViewOutputEquals(t, done(t).Stdout(), want) 764 } 765 766 func TestOperationJSON_planOutputChanges(t *testing.T) { 767 streams, done := terminal.StreamsForTesting(t) 768 v := &OperationJSON{view: NewJSONView(NewView(streams))} 769 770 root := addrs.RootModuleInstance 771 772 plan := &plans.Plan{ 773 Changes: &plans.Changes{ 774 Resources: []*plans.ResourceInstanceChangeSrc{}, 775 Outputs: []*plans.OutputChangeSrc{ 776 { 777 Addr: root.OutputValue("boop"), 778 ChangeSrc: plans.ChangeSrc{ 779 Action: plans.NoOp, 780 }, 781 }, 782 { 783 Addr: root.OutputValue("beep"), 784 ChangeSrc: plans.ChangeSrc{ 785 Action: plans.Create, 786 }, 787 }, 788 { 789 Addr: root.OutputValue("bonk"), 790 ChangeSrc: plans.ChangeSrc{ 791 Action: plans.Delete, 792 }, 793 }, 794 { 795 Addr: root.OutputValue("honk"), 796 ChangeSrc: plans.ChangeSrc{ 797 Action: plans.Update, 798 }, 799 Sensitive: true, 800 }, 801 }, 802 }, 803 } 804 v.Plan(plan, testSchemas()) 805 806 want := []map[string]interface{}{ 807 // No resource changes 808 { 809 "@level": "info", 810 "@message": "Plan: 0 to add, 0 to change, 0 to destroy.", 811 "@module": "terraform.ui", 812 "type": "change_summary", 813 "changes": map[string]interface{}{ 814 "operation": "plan", 815 "add": float64(0), 816 "change": float64(0), 817 "remove": float64(0), 818 }, 819 }, 820 // Output changes 821 { 822 "@level": "info", 823 "@message": "Outputs: 4", 824 "@module": "terraform.ui", 825 "type": "outputs", 826 "outputs": map[string]interface{}{ 827 "boop": map[string]interface{}{ 828 "action": "noop", 829 "sensitive": false, 830 }, 831 "beep": map[string]interface{}{ 832 "action": "create", 833 "sensitive": false, 834 }, 835 "bonk": map[string]interface{}{ 836 "action": "delete", 837 "sensitive": false, 838 }, 839 "honk": map[string]interface{}{ 840 "action": "update", 841 "sensitive": true, 842 }, 843 }, 844 }, 845 } 846 847 testJSONViewOutputEquals(t, done(t).Stdout(), want) 848 } 849 850 func TestOperationJSON_plannedChange(t *testing.T) { 851 streams, done := terminal.StreamsForTesting(t) 852 v := &OperationJSON{view: NewJSONView(NewView(streams))} 853 854 root := addrs.RootModuleInstance 855 boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "boop"} 856 derp := addrs.Resource{Mode: addrs.DataResourceMode, Type: "test_source", Name: "derp"} 857 858 // Replace requested by user 859 v.PlannedChange(&plans.ResourceInstanceChangeSrc{ 860 Addr: boop.Instance(addrs.IntKey(0)).Absolute(root), 861 ChangeSrc: plans.ChangeSrc{Action: plans.DeleteThenCreate}, 862 ActionReason: plans.ResourceInstanceReplaceByRequest, 863 }) 864 865 // Simple create 866 v.PlannedChange(&plans.ResourceInstanceChangeSrc{ 867 Addr: boop.Instance(addrs.IntKey(1)).Absolute(root), 868 ChangeSrc: plans.ChangeSrc{Action: plans.Create}, 869 }) 870 871 // Data source deletion 872 v.PlannedChange(&plans.ResourceInstanceChangeSrc{ 873 Addr: derp.Instance(addrs.NoKey).Absolute(root), 874 ChangeSrc: plans.ChangeSrc{Action: plans.Delete}, 875 }) 876 877 // Expect only two messages, as the data source deletion should be a no-op 878 want := []map[string]interface{}{ 879 { 880 "@level": "info", 881 "@message": "test_instance.boop[0]: Plan to replace", 882 "@module": "terraform.ui", 883 "type": "planned_change", 884 "change": map[string]interface{}{ 885 "action": "replace", 886 "reason": "requested", 887 "resource": map[string]interface{}{ 888 "addr": `test_instance.boop[0]`, 889 "implied_provider": "test", 890 "module": "", 891 "resource": `test_instance.boop[0]`, 892 "resource_key": float64(0), 893 "resource_name": "boop", 894 "resource_type": "test_instance", 895 }, 896 }, 897 }, 898 { 899 "@level": "info", 900 "@message": "test_instance.boop[1]: Plan to create", 901 "@module": "terraform.ui", 902 "type": "planned_change", 903 "change": map[string]interface{}{ 904 "action": "create", 905 "resource": map[string]interface{}{ 906 "addr": `test_instance.boop[1]`, 907 "implied_provider": "test", 908 "module": "", 909 "resource": `test_instance.boop[1]`, 910 "resource_key": float64(1), 911 "resource_name": "boop", 912 "resource_type": "test_instance", 913 }, 914 }, 915 }, 916 } 917 918 testJSONViewOutputEquals(t, done(t).Stdout(), want) 919 }