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