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