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  }