github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/views/operation_test.go (about)

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