github.com/hugorut/terraform@v1.1.3/src/command/views/operation_test.go (about)

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