github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/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/eliastor/durgaform/internal/addrs" 10 "github.com/eliastor/durgaform/internal/command/arguments" 11 "github.com/eliastor/durgaform/internal/lang/globalref" 12 "github.com/eliastor/durgaform/internal/plans" 13 "github.com/eliastor/durgaform/internal/states" 14 "github.com/eliastor/durgaform/internal/states/statefile" 15 "github.com/eliastor/durgaform/internal/terminal" 16 "github.com/eliastor/durgaform/internal/durgaform" 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 *durgaform.Schemas) *plans.Plan 82 wantText string 83 }{ 84 "nothing at all in normal mode": { 85 func(schemas *durgaform.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 *durgaform.Schemas) *plans.Plan { 95 return &plans.Plan{ 96 UIMode: plans.RefreshOnlyMode, 97 Changes: plans.NewChanges(), 98 } 99 }, 100 "Durgaform has checked that the real remote objects still match", 101 }, 102 "nothing at all in destroy mode": { 103 func(schemas *durgaform.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 *durgaform.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 *durgaform.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 Durgaform", 198 }, 199 "drift detected in refresh-only mode": { 200 func(schemas *durgaform.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 *durgaform.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 *durgaform.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 Durgaform used the selected providers to generate the following execution 340 plan. Resource actions are indicated with the following symbols: 341 + create 342 343 Durgaform 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_planNextStep(t *testing.T) { 360 testCases := map[string]struct { 361 path string 362 want string 363 }{ 364 "no state path": { 365 path: "", 366 want: "You didn't use the -out option", 367 }, 368 "state path": { 369 path: "good plan.tfplan", 370 want: `durgaform apply "good plan.tfplan"`, 371 }, 372 } 373 for name, tc := range testCases { 374 t.Run(name, func(t *testing.T) { 375 streams, done := terminal.StreamsForTesting(t) 376 v := NewOperation(arguments.ViewHuman, false, NewView(streams)) 377 378 v.PlanNextStep(tc.path) 379 380 if got := done(t).Stdout(); !strings.Contains(got, tc.want) { 381 t.Errorf("wrong result\ngot: %q\nwant: %q", got, tc.want) 382 } 383 }) 384 } 385 } 386 387 // The in-automation state is on the view itself, so testing it separately is 388 // clearer. 389 func TestOperation_planNextStepInAutomation(t *testing.T) { 390 streams, done := terminal.StreamsForTesting(t) 391 v := NewOperation(arguments.ViewHuman, true, NewView(streams)) 392 393 v.PlanNextStep("") 394 395 if got := done(t).Stdout(); got != "" { 396 t.Errorf("unexpected output\ngot: %q", got) 397 } 398 } 399 400 // Test all the trivial OperationJSON methods together. Y'know, for brevity. 401 // This test is not a realistic stream of messages. 402 func TestOperationJSON_logs(t *testing.T) { 403 streams, done := terminal.StreamsForTesting(t) 404 v := &OperationJSON{view: NewJSONView(NewView(streams))} 405 406 v.Cancelled(plans.NormalMode) 407 v.Cancelled(plans.DestroyMode) 408 v.Stopping() 409 v.Interrupted() 410 v.FatalInterrupt() 411 412 want := []map[string]interface{}{ 413 { 414 "@level": "info", 415 "@message": "Apply cancelled", 416 "@module": "durgaform.ui", 417 "type": "log", 418 }, 419 { 420 "@level": "info", 421 "@message": "Destroy cancelled", 422 "@module": "durgaform.ui", 423 "type": "log", 424 }, 425 { 426 "@level": "info", 427 "@message": "Stopping operation...", 428 "@module": "durgaform.ui", 429 "type": "log", 430 }, 431 { 432 "@level": "info", 433 "@message": interrupted, 434 "@module": "durgaform.ui", 435 "type": "log", 436 }, 437 { 438 "@level": "info", 439 "@message": fatalInterrupt, 440 "@module": "durgaform.ui", 441 "type": "log", 442 }, 443 } 444 445 testJSONViewOutputEquals(t, done(t).Stdout(), want) 446 } 447 448 // This is a fairly circular test, but it's such a rarely executed code path 449 // that I think it's probably still worth having. We're not testing against 450 // a fixed state JSON output because this test ought not fail just because 451 // we upgrade state format in the future. 452 func TestOperationJSON_emergencyDumpState(t *testing.T) { 453 streams, done := terminal.StreamsForTesting(t) 454 v := &OperationJSON{view: NewJSONView(NewView(streams))} 455 456 stateFile := statefile.New(nil, "foo", 1) 457 stateBuf := new(bytes.Buffer) 458 err := statefile.Write(stateFile, stateBuf) 459 if err != nil { 460 t.Fatal(err) 461 } 462 var stateJSON map[string]interface{} 463 err = json.Unmarshal(stateBuf.Bytes(), &stateJSON) 464 if err != nil { 465 t.Fatal(err) 466 } 467 468 err = v.EmergencyDumpState(stateFile) 469 if err != nil { 470 t.Fatalf("unexpected error dumping state: %s", err) 471 } 472 473 want := []map[string]interface{}{ 474 { 475 "@level": "info", 476 "@message": "Emergency state dump", 477 "@module": "durgaform.ui", 478 "type": "log", 479 "state": stateJSON, 480 }, 481 } 482 483 testJSONViewOutputEquals(t, done(t).Stdout(), want) 484 } 485 486 func TestOperationJSON_planNoChanges(t *testing.T) { 487 streams, done := terminal.StreamsForTesting(t) 488 v := &OperationJSON{view: NewJSONView(NewView(streams))} 489 490 plan := &plans.Plan{ 491 Changes: plans.NewChanges(), 492 } 493 v.Plan(plan, nil) 494 495 want := []map[string]interface{}{ 496 { 497 "@level": "info", 498 "@message": "Plan: 0 to add, 0 to change, 0 to destroy.", 499 "@module": "durgaform.ui", 500 "type": "change_summary", 501 "changes": map[string]interface{}{ 502 "operation": "plan", 503 "add": float64(0), 504 "change": float64(0), 505 "remove": float64(0), 506 }, 507 }, 508 } 509 510 testJSONViewOutputEquals(t, done(t).Stdout(), want) 511 } 512 513 func TestOperationJSON_plan(t *testing.T) { 514 streams, done := terminal.StreamsForTesting(t) 515 v := &OperationJSON{view: NewJSONView(NewView(streams))} 516 517 root := addrs.RootModuleInstance 518 vpc, diags := addrs.ParseModuleInstanceStr("module.vpc") 519 if len(diags) > 0 { 520 t.Fatal(diags.Err()) 521 } 522 boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"} 523 beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"} 524 derp := addrs.Resource{Mode: addrs.DataResourceMode, Type: "test_source", Name: "derp"} 525 526 plan := &plans.Plan{ 527 Changes: &plans.Changes{ 528 Resources: []*plans.ResourceInstanceChangeSrc{ 529 { 530 Addr: boop.Instance(addrs.IntKey(0)).Absolute(root), 531 PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(root), 532 ChangeSrc: plans.ChangeSrc{Action: plans.CreateThenDelete}, 533 }, 534 { 535 Addr: boop.Instance(addrs.IntKey(1)).Absolute(root), 536 PrevRunAddr: boop.Instance(addrs.IntKey(1)).Absolute(root), 537 ChangeSrc: plans.ChangeSrc{Action: plans.Create}, 538 }, 539 { 540 Addr: boop.Instance(addrs.IntKey(0)).Absolute(vpc), 541 PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(vpc), 542 ChangeSrc: plans.ChangeSrc{Action: plans.Delete}, 543 }, 544 { 545 Addr: beep.Instance(addrs.NoKey).Absolute(root), 546 PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root), 547 ChangeSrc: plans.ChangeSrc{Action: plans.DeleteThenCreate}, 548 }, 549 { 550 Addr: beep.Instance(addrs.NoKey).Absolute(vpc), 551 PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(vpc), 552 ChangeSrc: plans.ChangeSrc{Action: plans.Update}, 553 }, 554 // Data source deletion should not show up in the logs 555 { 556 Addr: derp.Instance(addrs.NoKey).Absolute(root), 557 PrevRunAddr: derp.Instance(addrs.NoKey).Absolute(root), 558 ChangeSrc: plans.ChangeSrc{Action: plans.Delete}, 559 }, 560 }, 561 }, 562 } 563 v.Plan(plan, testSchemas()) 564 565 want := []map[string]interface{}{ 566 // Create-then-delete should result in replace 567 { 568 "@level": "info", 569 "@message": "test_resource.boop[0]: Plan to replace", 570 "@module": "durgaform.ui", 571 "type": "planned_change", 572 "change": map[string]interface{}{ 573 "action": "replace", 574 "resource": map[string]interface{}{ 575 "addr": `test_resource.boop[0]`, 576 "implied_provider": "test", 577 "module": "", 578 "resource": `test_resource.boop[0]`, 579 "resource_key": float64(0), 580 "resource_name": "boop", 581 "resource_type": "test_resource", 582 }, 583 }, 584 }, 585 // Simple create 586 { 587 "@level": "info", 588 "@message": "test_resource.boop[1]: Plan to create", 589 "@module": "durgaform.ui", 590 "type": "planned_change", 591 "change": map[string]interface{}{ 592 "action": "create", 593 "resource": map[string]interface{}{ 594 "addr": `test_resource.boop[1]`, 595 "implied_provider": "test", 596 "module": "", 597 "resource": `test_resource.boop[1]`, 598 "resource_key": float64(1), 599 "resource_name": "boop", 600 "resource_type": "test_resource", 601 }, 602 }, 603 }, 604 // Simple delete 605 { 606 "@level": "info", 607 "@message": "module.vpc.test_resource.boop[0]: Plan to delete", 608 "@module": "durgaform.ui", 609 "type": "planned_change", 610 "change": map[string]interface{}{ 611 "action": "delete", 612 "resource": map[string]interface{}{ 613 "addr": `module.vpc.test_resource.boop[0]`, 614 "implied_provider": "test", 615 "module": "module.vpc", 616 "resource": `test_resource.boop[0]`, 617 "resource_key": float64(0), 618 "resource_name": "boop", 619 "resource_type": "test_resource", 620 }, 621 }, 622 }, 623 // Delete-then-create is also a replace 624 { 625 "@level": "info", 626 "@message": "test_resource.beep: Plan to replace", 627 "@module": "durgaform.ui", 628 "type": "planned_change", 629 "change": map[string]interface{}{ 630 "action": "replace", 631 "resource": map[string]interface{}{ 632 "addr": `test_resource.beep`, 633 "implied_provider": "test", 634 "module": "", 635 "resource": `test_resource.beep`, 636 "resource_key": nil, 637 "resource_name": "beep", 638 "resource_type": "test_resource", 639 }, 640 }, 641 }, 642 // Simple update 643 { 644 "@level": "info", 645 "@message": "module.vpc.test_resource.beep: Plan to update", 646 "@module": "durgaform.ui", 647 "type": "planned_change", 648 "change": map[string]interface{}{ 649 "action": "update", 650 "resource": map[string]interface{}{ 651 "addr": `module.vpc.test_resource.beep`, 652 "implied_provider": "test", 653 "module": "module.vpc", 654 "resource": `test_resource.beep`, 655 "resource_key": nil, 656 "resource_name": "beep", 657 "resource_type": "test_resource", 658 }, 659 }, 660 }, 661 // These counts are 3 add/1 change/3 destroy because the replace 662 // changes result in both add and destroy counts. 663 { 664 "@level": "info", 665 "@message": "Plan: 3 to add, 1 to change, 3 to destroy.", 666 "@module": "durgaform.ui", 667 "type": "change_summary", 668 "changes": map[string]interface{}{ 669 "operation": "plan", 670 "add": float64(3), 671 "change": float64(1), 672 "remove": float64(3), 673 }, 674 }, 675 } 676 677 testJSONViewOutputEquals(t, done(t).Stdout(), want) 678 } 679 680 func TestOperationJSON_planDriftWithMove(t *testing.T) { 681 streams, done := terminal.StreamsForTesting(t) 682 v := &OperationJSON{view: NewJSONView(NewView(streams))} 683 684 root := addrs.RootModuleInstance 685 boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"} 686 beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"} 687 blep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "blep"} 688 honk := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "honk"} 689 690 plan := &plans.Plan{ 691 UIMode: plans.NormalMode, 692 Changes: &plans.Changes{ 693 Resources: []*plans.ResourceInstanceChangeSrc{ 694 { 695 Addr: honk.Instance(addrs.StringKey("bonk")).Absolute(root), 696 PrevRunAddr: honk.Instance(addrs.IntKey(0)).Absolute(root), 697 ChangeSrc: plans.ChangeSrc{Action: plans.NoOp}, 698 }, 699 }, 700 }, 701 DriftedResources: []*plans.ResourceInstanceChangeSrc{ 702 { 703 Addr: beep.Instance(addrs.NoKey).Absolute(root), 704 PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root), 705 ChangeSrc: plans.ChangeSrc{Action: plans.Delete}, 706 }, 707 { 708 Addr: boop.Instance(addrs.NoKey).Absolute(root), 709 PrevRunAddr: blep.Instance(addrs.NoKey).Absolute(root), 710 ChangeSrc: plans.ChangeSrc{Action: plans.Update}, 711 }, 712 // Move-only resource drift should not be present in normal mode plans 713 { 714 Addr: honk.Instance(addrs.StringKey("bonk")).Absolute(root), 715 PrevRunAddr: honk.Instance(addrs.IntKey(0)).Absolute(root), 716 ChangeSrc: plans.ChangeSrc{Action: plans.NoOp}, 717 }, 718 }, 719 } 720 v.Plan(plan, testSchemas()) 721 722 want := []map[string]interface{}{ 723 // Drift detected: delete 724 { 725 "@level": "info", 726 "@message": "test_resource.beep: Drift detected (delete)", 727 "@module": "durgaform.ui", 728 "type": "resource_drift", 729 "change": map[string]interface{}{ 730 "action": "delete", 731 "resource": map[string]interface{}{ 732 "addr": "test_resource.beep", 733 "implied_provider": "test", 734 "module": "", 735 "resource": "test_resource.beep", 736 "resource_key": nil, 737 "resource_name": "beep", 738 "resource_type": "test_resource", 739 }, 740 }, 741 }, 742 // Drift detected: update with move 743 { 744 "@level": "info", 745 "@message": "test_resource.boop: Drift detected (update)", 746 "@module": "durgaform.ui", 747 "type": "resource_drift", 748 "change": map[string]interface{}{ 749 "action": "update", 750 "resource": map[string]interface{}{ 751 "addr": "test_resource.boop", 752 "implied_provider": "test", 753 "module": "", 754 "resource": "test_resource.boop", 755 "resource_key": nil, 756 "resource_name": "boop", 757 "resource_type": "test_resource", 758 }, 759 "previous_resource": map[string]interface{}{ 760 "addr": "test_resource.blep", 761 "implied_provider": "test", 762 "module": "", 763 "resource": "test_resource.blep", 764 "resource_key": nil, 765 "resource_name": "blep", 766 "resource_type": "test_resource", 767 }, 768 }, 769 }, 770 // Move-only change 771 { 772 "@level": "info", 773 "@message": `test_resource.honk["bonk"]: Plan to move`, 774 "@module": "durgaform.ui", 775 "type": "planned_change", 776 "change": map[string]interface{}{ 777 "action": "move", 778 "resource": map[string]interface{}{ 779 "addr": `test_resource.honk["bonk"]`, 780 "implied_provider": "test", 781 "module": "", 782 "resource": `test_resource.honk["bonk"]`, 783 "resource_key": "bonk", 784 "resource_name": "honk", 785 "resource_type": "test_resource", 786 }, 787 "previous_resource": map[string]interface{}{ 788 "addr": `test_resource.honk[0]`, 789 "implied_provider": "test", 790 "module": "", 791 "resource": `test_resource.honk[0]`, 792 "resource_key": float64(0), 793 "resource_name": "honk", 794 "resource_type": "test_resource", 795 }, 796 }, 797 }, 798 // No changes 799 { 800 "@level": "info", 801 "@message": "Plan: 0 to add, 0 to change, 0 to destroy.", 802 "@module": "durgaform.ui", 803 "type": "change_summary", 804 "changes": map[string]interface{}{ 805 "operation": "plan", 806 "add": float64(0), 807 "change": float64(0), 808 "remove": float64(0), 809 }, 810 }, 811 } 812 813 testJSONViewOutputEquals(t, done(t).Stdout(), want) 814 } 815 816 func TestOperationJSON_planDriftWithMoveRefreshOnly(t *testing.T) { 817 streams, done := terminal.StreamsForTesting(t) 818 v := &OperationJSON{view: NewJSONView(NewView(streams))} 819 820 root := addrs.RootModuleInstance 821 boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"} 822 beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"} 823 blep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "blep"} 824 honk := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "honk"} 825 826 plan := &plans.Plan{ 827 UIMode: plans.RefreshOnlyMode, 828 Changes: &plans.Changes{ 829 Resources: []*plans.ResourceInstanceChangeSrc{}, 830 }, 831 DriftedResources: []*plans.ResourceInstanceChangeSrc{ 832 { 833 Addr: beep.Instance(addrs.NoKey).Absolute(root), 834 PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root), 835 ChangeSrc: plans.ChangeSrc{Action: plans.Delete}, 836 }, 837 { 838 Addr: boop.Instance(addrs.NoKey).Absolute(root), 839 PrevRunAddr: blep.Instance(addrs.NoKey).Absolute(root), 840 ChangeSrc: plans.ChangeSrc{Action: plans.Update}, 841 }, 842 // Move-only resource drift should be present in refresh-only plans 843 { 844 Addr: honk.Instance(addrs.StringKey("bonk")).Absolute(root), 845 PrevRunAddr: honk.Instance(addrs.IntKey(0)).Absolute(root), 846 ChangeSrc: plans.ChangeSrc{Action: plans.NoOp}, 847 }, 848 }, 849 } 850 v.Plan(plan, testSchemas()) 851 852 want := []map[string]interface{}{ 853 // Drift detected: delete 854 { 855 "@level": "info", 856 "@message": "test_resource.beep: Drift detected (delete)", 857 "@module": "durgaform.ui", 858 "type": "resource_drift", 859 "change": map[string]interface{}{ 860 "action": "delete", 861 "resource": map[string]interface{}{ 862 "addr": "test_resource.beep", 863 "implied_provider": "test", 864 "module": "", 865 "resource": "test_resource.beep", 866 "resource_key": nil, 867 "resource_name": "beep", 868 "resource_type": "test_resource", 869 }, 870 }, 871 }, 872 // Drift detected: update 873 { 874 "@level": "info", 875 "@message": "test_resource.boop: Drift detected (update)", 876 "@module": "durgaform.ui", 877 "type": "resource_drift", 878 "change": map[string]interface{}{ 879 "action": "update", 880 "resource": map[string]interface{}{ 881 "addr": "test_resource.boop", 882 "implied_provider": "test", 883 "module": "", 884 "resource": "test_resource.boop", 885 "resource_key": nil, 886 "resource_name": "boop", 887 "resource_type": "test_resource", 888 }, 889 "previous_resource": map[string]interface{}{ 890 "addr": "test_resource.blep", 891 "implied_provider": "test", 892 "module": "", 893 "resource": "test_resource.blep", 894 "resource_key": nil, 895 "resource_name": "blep", 896 "resource_type": "test_resource", 897 }, 898 }, 899 }, 900 // Drift detected: Move-only change 901 { 902 "@level": "info", 903 "@message": `test_resource.honk["bonk"]: Drift detected (move)`, 904 "@module": "durgaform.ui", 905 "type": "resource_drift", 906 "change": map[string]interface{}{ 907 "action": "move", 908 "resource": map[string]interface{}{ 909 "addr": `test_resource.honk["bonk"]`, 910 "implied_provider": "test", 911 "module": "", 912 "resource": `test_resource.honk["bonk"]`, 913 "resource_key": "bonk", 914 "resource_name": "honk", 915 "resource_type": "test_resource", 916 }, 917 "previous_resource": map[string]interface{}{ 918 "addr": `test_resource.honk[0]`, 919 "implied_provider": "test", 920 "module": "", 921 "resource": `test_resource.honk[0]`, 922 "resource_key": float64(0), 923 "resource_name": "honk", 924 "resource_type": "test_resource", 925 }, 926 }, 927 }, 928 // No changes 929 { 930 "@level": "info", 931 "@message": "Plan: 0 to add, 0 to change, 0 to destroy.", 932 "@module": "durgaform.ui", 933 "type": "change_summary", 934 "changes": map[string]interface{}{ 935 "operation": "plan", 936 "add": float64(0), 937 "change": float64(0), 938 "remove": float64(0), 939 }, 940 }, 941 } 942 943 testJSONViewOutputEquals(t, done(t).Stdout(), want) 944 } 945 946 func TestOperationJSON_planOutputChanges(t *testing.T) { 947 streams, done := terminal.StreamsForTesting(t) 948 v := &OperationJSON{view: NewJSONView(NewView(streams))} 949 950 root := addrs.RootModuleInstance 951 952 plan := &plans.Plan{ 953 Changes: &plans.Changes{ 954 Resources: []*plans.ResourceInstanceChangeSrc{}, 955 Outputs: []*plans.OutputChangeSrc{ 956 { 957 Addr: root.OutputValue("boop"), 958 ChangeSrc: plans.ChangeSrc{ 959 Action: plans.NoOp, 960 }, 961 }, 962 { 963 Addr: root.OutputValue("beep"), 964 ChangeSrc: plans.ChangeSrc{ 965 Action: plans.Create, 966 }, 967 }, 968 { 969 Addr: root.OutputValue("bonk"), 970 ChangeSrc: plans.ChangeSrc{ 971 Action: plans.Delete, 972 }, 973 }, 974 { 975 Addr: root.OutputValue("honk"), 976 ChangeSrc: plans.ChangeSrc{ 977 Action: plans.Update, 978 }, 979 Sensitive: true, 980 }, 981 }, 982 }, 983 } 984 v.Plan(plan, testSchemas()) 985 986 want := []map[string]interface{}{ 987 // No resource changes 988 { 989 "@level": "info", 990 "@message": "Plan: 0 to add, 0 to change, 0 to destroy.", 991 "@module": "durgaform.ui", 992 "type": "change_summary", 993 "changes": map[string]interface{}{ 994 "operation": "plan", 995 "add": float64(0), 996 "change": float64(0), 997 "remove": float64(0), 998 }, 999 }, 1000 // Output changes 1001 { 1002 "@level": "info", 1003 "@message": "Outputs: 4", 1004 "@module": "durgaform.ui", 1005 "type": "outputs", 1006 "outputs": map[string]interface{}{ 1007 "boop": map[string]interface{}{ 1008 "action": "noop", 1009 "sensitive": false, 1010 }, 1011 "beep": map[string]interface{}{ 1012 "action": "create", 1013 "sensitive": false, 1014 }, 1015 "bonk": map[string]interface{}{ 1016 "action": "delete", 1017 "sensitive": false, 1018 }, 1019 "honk": map[string]interface{}{ 1020 "action": "update", 1021 "sensitive": true, 1022 }, 1023 }, 1024 }, 1025 } 1026 1027 testJSONViewOutputEquals(t, done(t).Stdout(), want) 1028 } 1029 1030 func TestOperationJSON_plannedChange(t *testing.T) { 1031 streams, done := terminal.StreamsForTesting(t) 1032 v := &OperationJSON{view: NewJSONView(NewView(streams))} 1033 1034 root := addrs.RootModuleInstance 1035 boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "boop"} 1036 derp := addrs.Resource{Mode: addrs.DataResourceMode, Type: "test_source", Name: "derp"} 1037 1038 // Replace requested by user 1039 v.PlannedChange(&plans.ResourceInstanceChangeSrc{ 1040 Addr: boop.Instance(addrs.IntKey(0)).Absolute(root), 1041 PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(root), 1042 ChangeSrc: plans.ChangeSrc{Action: plans.DeleteThenCreate}, 1043 ActionReason: plans.ResourceInstanceReplaceByRequest, 1044 }) 1045 1046 // Simple create 1047 v.PlannedChange(&plans.ResourceInstanceChangeSrc{ 1048 Addr: boop.Instance(addrs.IntKey(1)).Absolute(root), 1049 PrevRunAddr: boop.Instance(addrs.IntKey(1)).Absolute(root), 1050 ChangeSrc: plans.ChangeSrc{Action: plans.Create}, 1051 }) 1052 1053 // Data source deletion 1054 v.PlannedChange(&plans.ResourceInstanceChangeSrc{ 1055 Addr: derp.Instance(addrs.NoKey).Absolute(root), 1056 PrevRunAddr: derp.Instance(addrs.NoKey).Absolute(root), 1057 ChangeSrc: plans.ChangeSrc{Action: plans.Delete}, 1058 }) 1059 1060 // Expect only two messages, as the data source deletion should be a no-op 1061 want := []map[string]interface{}{ 1062 { 1063 "@level": "info", 1064 "@message": "test_instance.boop[0]: Plan to replace", 1065 "@module": "durgaform.ui", 1066 "type": "planned_change", 1067 "change": map[string]interface{}{ 1068 "action": "replace", 1069 "reason": "requested", 1070 "resource": map[string]interface{}{ 1071 "addr": `test_instance.boop[0]`, 1072 "implied_provider": "test", 1073 "module": "", 1074 "resource": `test_instance.boop[0]`, 1075 "resource_key": float64(0), 1076 "resource_name": "boop", 1077 "resource_type": "test_instance", 1078 }, 1079 }, 1080 }, 1081 { 1082 "@level": "info", 1083 "@message": "test_instance.boop[1]: Plan to create", 1084 "@module": "durgaform.ui", 1085 "type": "planned_change", 1086 "change": map[string]interface{}{ 1087 "action": "create", 1088 "resource": map[string]interface{}{ 1089 "addr": `test_instance.boop[1]`, 1090 "implied_provider": "test", 1091 "module": "", 1092 "resource": `test_instance.boop[1]`, 1093 "resource_key": float64(1), 1094 "resource_name": "boop", 1095 "resource_type": "test_instance", 1096 }, 1097 }, 1098 }, 1099 } 1100 1101 testJSONViewOutputEquals(t, done(t).Stdout(), want) 1102 }