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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package jsonformat
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"testing"
    10  
    11  	"github.com/google/go-cmp/cmp"
    12  	"github.com/mitchellh/colorstring"
    13  	"github.com/zclconf/go-cty/cty"
    14  
    15  	"github.com/terramate-io/tf/addrs"
    16  	"github.com/terramate-io/tf/command/jsonformat/differ"
    17  	"github.com/terramate-io/tf/command/jsonformat/structured"
    18  	"github.com/terramate-io/tf/command/jsonformat/structured/attribute_path"
    19  	"github.com/terramate-io/tf/command/jsonplan"
    20  	"github.com/terramate-io/tf/command/jsonprovider"
    21  	"github.com/terramate-io/tf/configs/configschema"
    22  	"github.com/terramate-io/tf/lang/marks"
    23  	"github.com/terramate-io/tf/plans"
    24  	"github.com/terramate-io/tf/providers"
    25  	"github.com/terramate-io/tf/states"
    26  	"github.com/terramate-io/tf/terminal"
    27  	"github.com/terramate-io/tf/terraform"
    28  )
    29  
    30  func TestRenderHuman_EmptyPlan(t *testing.T) {
    31  	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
    32  	streams, done := terminal.StreamsForTesting(t)
    33  
    34  	plan := Plan{}
    35  
    36  	renderer := Renderer{Colorize: color, Streams: streams}
    37  	plan.renderHuman(renderer, plans.NormalMode)
    38  
    39  	want := `
    40  No changes. Your infrastructure matches the configuration.
    41  
    42  Terraform has compared your real infrastructure against your configuration
    43  and found no differences, so no changes are needed.
    44  `
    45  
    46  	got := done(t).Stdout()
    47  	if diff := cmp.Diff(want, got); len(diff) > 0 {
    48  		t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
    49  	}
    50  }
    51  
    52  func TestRenderHuman_EmptyOutputs(t *testing.T) {
    53  	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
    54  	streams, done := terminal.StreamsForTesting(t)
    55  
    56  	outputVal, _ := json.Marshal("some-text")
    57  	plan := Plan{
    58  		OutputChanges: map[string]jsonplan.Change{
    59  			"a_string": {
    60  				Actions: []string{"no-op"},
    61  				Before:  outputVal,
    62  				After:   outputVal,
    63  			},
    64  		},
    65  	}
    66  
    67  	renderer := Renderer{Colorize: color, Streams: streams}
    68  	plan.renderHuman(renderer, plans.NormalMode)
    69  
    70  	want := `
    71  No changes. Your infrastructure matches the configuration.
    72  
    73  Terraform has compared your real infrastructure against your configuration
    74  and found no differences, so no changes are needed.
    75  `
    76  
    77  	got := done(t).Stdout()
    78  	if diff := cmp.Diff(want, got); len(diff) > 0 {
    79  		t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
    80  	}
    81  }
    82  
    83  func TestRenderHuman_Imports(t *testing.T) {
    84  	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
    85  
    86  	schemas := map[string]*jsonprovider.Provider{
    87  		"test": {
    88  			ResourceSchemas: map[string]*jsonprovider.Schema{
    89  				"test_resource": {
    90  					Block: &jsonprovider.Block{
    91  						Attributes: map[string]*jsonprovider.Attribute{
    92  							"id": {
    93  								AttributeType: marshalJson(t, "string"),
    94  							},
    95  							"value": {
    96  								AttributeType: marshalJson(t, "string"),
    97  							},
    98  						},
    99  					},
   100  				},
   101  			},
   102  		},
   103  	}
   104  
   105  	tcs := map[string]struct {
   106  		plan   Plan
   107  		output string
   108  	}{
   109  		"simple_import": {
   110  			plan: Plan{
   111  				ResourceChanges: []jsonplan.ResourceChange{
   112  					{
   113  						Address:      "test_resource.resource",
   114  						Mode:         "managed",
   115  						Type:         "test_resource",
   116  						Name:         "resource",
   117  						ProviderName: "test",
   118  						Change: jsonplan.Change{
   119  							Actions: []string{"no-op"},
   120  							Before: marshalJson(t, map[string]interface{}{
   121  								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   122  								"value": "Hello, World!",
   123  							}),
   124  							After: marshalJson(t, map[string]interface{}{
   125  								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   126  								"value": "Hello, World!",
   127  							}),
   128  							Importing: &jsonplan.Importing{
   129  								ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   130  							},
   131  						},
   132  					},
   133  				},
   134  			},
   135  			output: `
   136  Terraform will perform the following actions:
   137  
   138    # test_resource.resource will be imported
   139      resource "test_resource" "resource" {
   140          id    = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
   141          value = "Hello, World!"
   142      }
   143  
   144  Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
   145  `,
   146  		},
   147  		"simple_import_with_generated_config": {
   148  			plan: Plan{
   149  				ResourceChanges: []jsonplan.ResourceChange{
   150  					{
   151  						Address:      "test_resource.resource",
   152  						Mode:         "managed",
   153  						Type:         "test_resource",
   154  						Name:         "resource",
   155  						ProviderName: "test",
   156  						Change: jsonplan.Change{
   157  							Actions: []string{"no-op"},
   158  							Before: marshalJson(t, map[string]interface{}{
   159  								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   160  								"value": "Hello, World!",
   161  							}),
   162  							After: marshalJson(t, map[string]interface{}{
   163  								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   164  								"value": "Hello, World!",
   165  							}),
   166  							Importing: &jsonplan.Importing{
   167  								ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   168  							},
   169  							GeneratedConfig: `resource "test_resource" "resource" {
   170    id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
   171    value = "Hello, World!"
   172  }`,
   173  						},
   174  					},
   175  				},
   176  			},
   177  			output: `
   178  Terraform will perform the following actions:
   179  
   180    # test_resource.resource will be imported
   181    # (config will be generated)
   182      resource "test_resource" "resource" {
   183          id    = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
   184          value = "Hello, World!"
   185      }
   186  
   187  Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
   188  `,
   189  		},
   190  		"import_and_move": {
   191  			plan: Plan{
   192  				ResourceChanges: []jsonplan.ResourceChange{
   193  					{
   194  						Address:         "test_resource.after",
   195  						PreviousAddress: "test_resource.before",
   196  						Mode:            "managed",
   197  						Type:            "test_resource",
   198  						Name:            "after",
   199  						ProviderName:    "test",
   200  						Change: jsonplan.Change{
   201  							Actions: []string{"no-op"},
   202  							Before: marshalJson(t, map[string]interface{}{
   203  								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   204  								"value": "Hello, World!",
   205  							}),
   206  							After: marshalJson(t, map[string]interface{}{
   207  								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   208  								"value": "Hello, World!",
   209  							}),
   210  							Importing: &jsonplan.Importing{
   211  								ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   212  							},
   213  						},
   214  					},
   215  				},
   216  			},
   217  			output: `
   218  Terraform will perform the following actions:
   219  
   220    # test_resource.before has moved to test_resource.after
   221    # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E")
   222      resource "test_resource" "after" {
   223          id    = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
   224          value = "Hello, World!"
   225      }
   226  
   227  Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
   228  `,
   229  		},
   230  		"import_move_and_update": {
   231  			plan: Plan{
   232  				ResourceChanges: []jsonplan.ResourceChange{
   233  					{
   234  						Address:         "test_resource.after",
   235  						PreviousAddress: "test_resource.before",
   236  						Mode:            "managed",
   237  						Type:            "test_resource",
   238  						Name:            "after",
   239  						ProviderName:    "test",
   240  						Change: jsonplan.Change{
   241  							Actions: []string{"update"},
   242  							Before: marshalJson(t, map[string]interface{}{
   243  								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   244  								"value": "Hello, World!",
   245  							}),
   246  							After: marshalJson(t, map[string]interface{}{
   247  								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   248  								"value": "Hello, Universe!",
   249  							}),
   250  							Importing: &jsonplan.Importing{
   251  								ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   252  							},
   253  						},
   254  					},
   255  				},
   256  			},
   257  			output: `
   258  Terraform used the selected providers to generate the following execution
   259  plan. Resource actions are indicated with the following symbols:
   260    ~ update in-place
   261  
   262  Terraform will perform the following actions:
   263  
   264    # test_resource.after will be updated in-place
   265    # (moved from test_resource.before)
   266    # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E")
   267    ~ resource "test_resource" "after" {
   268          id    = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
   269        ~ value = "Hello, World!" -> "Hello, Universe!"
   270      }
   271  
   272  Plan: 1 to import, 0 to add, 1 to change, 0 to destroy.
   273  `,
   274  		},
   275  		"import_and_update": {
   276  			plan: Plan{
   277  				ResourceChanges: []jsonplan.ResourceChange{
   278  					{
   279  						Address:      "test_resource.resource",
   280  						Mode:         "managed",
   281  						Type:         "test_resource",
   282  						Name:         "resource",
   283  						ProviderName: "test",
   284  						Change: jsonplan.Change{
   285  							Actions: []string{"update"},
   286  							Before: marshalJson(t, map[string]interface{}{
   287  								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   288  								"value": "Hello, World!",
   289  							}),
   290  							After: marshalJson(t, map[string]interface{}{
   291  								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   292  								"value": "Hello, Universe!",
   293  							}),
   294  							Importing: &jsonplan.Importing{
   295  								ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   296  							},
   297  						},
   298  					},
   299  				},
   300  			},
   301  			output: `
   302  Terraform used the selected providers to generate the following execution
   303  plan. Resource actions are indicated with the following symbols:
   304    ~ update in-place
   305  
   306  Terraform will perform the following actions:
   307  
   308    # test_resource.resource will be updated in-place
   309    # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E")
   310    ~ resource "test_resource" "resource" {
   311          id    = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
   312        ~ value = "Hello, World!" -> "Hello, Universe!"
   313      }
   314  
   315  Plan: 1 to import, 0 to add, 1 to change, 0 to destroy.
   316  `,
   317  		},
   318  		"import_and_update_with_no_id": {
   319  			plan: Plan{
   320  				ResourceChanges: []jsonplan.ResourceChange{
   321  					{
   322  						Address:      "test_resource.resource",
   323  						Mode:         "managed",
   324  						Type:         "test_resource",
   325  						Name:         "resource",
   326  						ProviderName: "test",
   327  						Change: jsonplan.Change{
   328  							Actions: []string{"update"},
   329  							Before: marshalJson(t, map[string]interface{}{
   330  								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   331  								"value": "Hello, World!",
   332  							}),
   333  							After: marshalJson(t, map[string]interface{}{
   334  								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   335  								"value": "Hello, Universe!",
   336  							}),
   337  							Importing: &jsonplan.Importing{},
   338  						},
   339  					},
   340  				},
   341  			},
   342  			output: `
   343  Terraform used the selected providers to generate the following execution
   344  plan. Resource actions are indicated with the following symbols:
   345    ~ update in-place
   346  
   347  Terraform will perform the following actions:
   348  
   349    # test_resource.resource will be updated in-place
   350    # (will be imported first)
   351    ~ resource "test_resource" "resource" {
   352          id    = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
   353        ~ value = "Hello, World!" -> "Hello, Universe!"
   354      }
   355  
   356  Plan: 1 to import, 0 to add, 1 to change, 0 to destroy.
   357  `,
   358  		},
   359  		"import_and_replace": {
   360  			plan: Plan{
   361  				ResourceChanges: []jsonplan.ResourceChange{
   362  					{
   363  						Address:      "test_resource.resource",
   364  						Mode:         "managed",
   365  						Type:         "test_resource",
   366  						Name:         "resource",
   367  						ProviderName: "test",
   368  						Change: jsonplan.Change{
   369  							Actions: []string{"create", "delete"},
   370  							Before: marshalJson(t, map[string]interface{}{
   371  								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   372  								"value": "Hello, World!",
   373  							}),
   374  							After: marshalJson(t, map[string]interface{}{
   375  								"id":    "9794FB1F-7260-442F-830C-F2D450E90CE3",
   376  								"value": "Hello, World!",
   377  							}),
   378  							ReplacePaths: marshalJson(t, [][]string{{"id"}}),
   379  							Importing: &jsonplan.Importing{
   380  								ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
   381  							},
   382  						},
   383  						ActionReason: "",
   384  					},
   385  				},
   386  			},
   387  			output: `
   388  Terraform used the selected providers to generate the following execution
   389  plan. Resource actions are indicated with the following symbols:
   390  +/- create replacement and then destroy
   391  
   392  Terraform will perform the following actions:
   393  
   394    # test_resource.resource must be replaced
   395    # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E")
   396    # Warning: this will destroy the imported resource
   397  +/- resource "test_resource" "resource" {
   398        ~ id    = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" -> "9794FB1F-7260-442F-830C-F2D450E90CE3" # forces replacement
   399          value = "Hello, World!"
   400      }
   401  
   402  Plan: 1 to import, 1 to add, 0 to change, 1 to destroy.
   403  `,
   404  		},
   405  	}
   406  	for name, tc := range tcs {
   407  		t.Run(name, func(t *testing.T) {
   408  			streams, done := terminal.StreamsForTesting(t)
   409  
   410  			plan := tc.plan
   411  			plan.PlanFormatVersion = jsonplan.FormatVersion
   412  			plan.ProviderFormatVersion = jsonprovider.FormatVersion
   413  			plan.ProviderSchemas = schemas
   414  
   415  			renderer := Renderer{
   416  				Colorize: color,
   417  				Streams:  streams,
   418  			}
   419  			plan.renderHuman(renderer, plans.NormalMode)
   420  
   421  			got := done(t).Stdout()
   422  			want := tc.output
   423  			if diff := cmp.Diff(want, got); len(diff) > 0 {
   424  				t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
   425  			}
   426  		})
   427  	}
   428  }
   429  
   430  func TestResourceChange_primitiveTypes(t *testing.T) {
   431  	testCases := map[string]testCase{
   432  		"creation": {
   433  			Action: plans.Create,
   434  			Mode:   addrs.ManagedResourceMode,
   435  			Before: cty.NullVal(cty.EmptyObject),
   436  			After: cty.ObjectVal(map[string]cty.Value{
   437  				"id": cty.UnknownVal(cty.String),
   438  			}),
   439  			Schema: &configschema.Block{
   440  				Attributes: map[string]*configschema.Attribute{
   441  					"id": {Type: cty.String, Computed: true},
   442  				},
   443  			},
   444  			RequiredReplace: cty.NewPathSet(),
   445  			ExpectedOutput: `  # test_instance.example will be created
   446    + resource "test_instance" "example" {
   447        + id = (known after apply)
   448      }`,
   449  		},
   450  		"creation (null string)": {
   451  			Action: plans.Create,
   452  			Mode:   addrs.ManagedResourceMode,
   453  			Before: cty.NullVal(cty.EmptyObject),
   454  			After: cty.ObjectVal(map[string]cty.Value{
   455  				"string": cty.StringVal("null"),
   456  			}),
   457  			Schema: &configschema.Block{
   458  				Attributes: map[string]*configschema.Attribute{
   459  					"string": {Type: cty.String, Optional: true},
   460  				},
   461  			},
   462  			RequiredReplace: cty.NewPathSet(),
   463  			ExpectedOutput: `  # test_instance.example will be created
   464    + resource "test_instance" "example" {
   465        + string = "null"
   466      }`,
   467  		},
   468  		"creation (null string with extra whitespace)": {
   469  			Action: plans.Create,
   470  			Mode:   addrs.ManagedResourceMode,
   471  			Before: cty.NullVal(cty.EmptyObject),
   472  			After: cty.ObjectVal(map[string]cty.Value{
   473  				"string": cty.StringVal("null "),
   474  			}),
   475  			Schema: &configschema.Block{
   476  				Attributes: map[string]*configschema.Attribute{
   477  					"string": {Type: cty.String, Optional: true},
   478  				},
   479  			},
   480  			RequiredReplace: cty.NewPathSet(),
   481  			ExpectedOutput: `  # test_instance.example will be created
   482    + resource "test_instance" "example" {
   483        + string = "null "
   484      }`,
   485  		},
   486  		"creation (object with quoted keys)": {
   487  			Action: plans.Create,
   488  			Mode:   addrs.ManagedResourceMode,
   489  			Before: cty.NullVal(cty.EmptyObject),
   490  			After: cty.ObjectVal(map[string]cty.Value{
   491  				"object": cty.ObjectVal(map[string]cty.Value{
   492  					"unquoted":   cty.StringVal("value"),
   493  					"quoted:key": cty.StringVal("some-value"),
   494  				}),
   495  			}),
   496  			Schema: &configschema.Block{
   497  				Attributes: map[string]*configschema.Attribute{
   498  					"object": {Type: cty.Object(map[string]cty.Type{
   499  						"unquoted":   cty.String,
   500  						"quoted:key": cty.String,
   501  					}), Optional: true},
   502  				},
   503  			},
   504  			RequiredReplace: cty.NewPathSet(),
   505  			ExpectedOutput: `  # test_instance.example will be created
   506    + resource "test_instance" "example" {
   507        + object = {
   508            + "quoted:key" = "some-value"
   509            + unquoted     = "value"
   510          }
   511      }`,
   512  		},
   513  		"deletion": {
   514  			Action: plans.Delete,
   515  			Mode:   addrs.ManagedResourceMode,
   516  			Before: cty.ObjectVal(map[string]cty.Value{
   517  				"id": cty.StringVal("i-02ae66f368e8518a9"),
   518  			}),
   519  			After: cty.NullVal(cty.EmptyObject),
   520  			Schema: &configschema.Block{
   521  				Attributes: map[string]*configschema.Attribute{
   522  					"id": {Type: cty.String, Computed: true},
   523  				},
   524  			},
   525  			RequiredReplace: cty.NewPathSet(),
   526  			ExpectedOutput: `  # test_instance.example will be destroyed
   527    - resource "test_instance" "example" {
   528        - id = "i-02ae66f368e8518a9" -> null
   529      }`,
   530  		},
   531  		"deletion of deposed object": {
   532  			Action:     plans.Delete,
   533  			Mode:       addrs.ManagedResourceMode,
   534  			DeposedKey: states.DeposedKey("byebye"),
   535  			Before: cty.ObjectVal(map[string]cty.Value{
   536  				"id": cty.StringVal("i-02ae66f368e8518a9"),
   537  			}),
   538  			After: cty.NullVal(cty.EmptyObject),
   539  			Schema: &configschema.Block{
   540  				Attributes: map[string]*configschema.Attribute{
   541  					"id": {Type: cty.String, Computed: true},
   542  				},
   543  			},
   544  			RequiredReplace: cty.NewPathSet(),
   545  			ExpectedOutput: `  # test_instance.example (deposed object byebye) will be destroyed
   546    # (left over from a partially-failed replacement of this instance)
   547    - resource "test_instance" "example" {
   548        - id = "i-02ae66f368e8518a9" -> null
   549      }`,
   550  		},
   551  		"deletion (empty string)": {
   552  			Action: plans.Delete,
   553  			Mode:   addrs.ManagedResourceMode,
   554  			Before: cty.ObjectVal(map[string]cty.Value{
   555  				"id":                 cty.StringVal("i-02ae66f368e8518a9"),
   556  				"intentionally_long": cty.StringVal(""),
   557  			}),
   558  			After: cty.NullVal(cty.EmptyObject),
   559  			Schema: &configschema.Block{
   560  				Attributes: map[string]*configschema.Attribute{
   561  					"id":                 {Type: cty.String, Computed: true},
   562  					"intentionally_long": {Type: cty.String, Optional: true},
   563  				},
   564  			},
   565  			RequiredReplace: cty.NewPathSet(),
   566  			ExpectedOutput: `  # test_instance.example will be destroyed
   567    - resource "test_instance" "example" {
   568        - id = "i-02ae66f368e8518a9" -> null
   569      }`,
   570  		},
   571  		"string in-place update": {
   572  			Action: plans.Update,
   573  			Mode:   addrs.ManagedResourceMode,
   574  			Before: cty.ObjectVal(map[string]cty.Value{
   575  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   576  				"ami": cty.StringVal("ami-BEFORE"),
   577  			}),
   578  			After: cty.ObjectVal(map[string]cty.Value{
   579  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   580  				"ami": cty.StringVal("ami-AFTER"),
   581  			}),
   582  			Schema: &configschema.Block{
   583  				Attributes: map[string]*configschema.Attribute{
   584  					"id":  {Type: cty.String, Optional: true, Computed: true},
   585  					"ami": {Type: cty.String, Optional: true},
   586  				},
   587  			},
   588  			RequiredReplace: cty.NewPathSet(),
   589  			ExpectedOutput: `  # test_instance.example will be updated in-place
   590    ~ resource "test_instance" "example" {
   591        ~ ami = "ami-BEFORE" -> "ami-AFTER"
   592          id  = "i-02ae66f368e8518a9"
   593      }`,
   594  		},
   595  		"update with quoted key": {
   596  			Action: plans.Update,
   597  			Mode:   addrs.ManagedResourceMode,
   598  			Before: cty.ObjectVal(map[string]cty.Value{
   599  				"id":       cty.StringVal("i-02ae66f368e8518a9"),
   600  				"saml:aud": cty.StringVal("https://example.com/saml"),
   601  				"zeta":     cty.StringVal("alpha"),
   602  			}),
   603  			After: cty.ObjectVal(map[string]cty.Value{
   604  				"id":       cty.StringVal("i-02ae66f368e8518a9"),
   605  				"saml:aud": cty.StringVal("https://saml.example.com"),
   606  				"zeta":     cty.StringVal("alpha"),
   607  			}),
   608  			Schema: &configschema.Block{
   609  				Attributes: map[string]*configschema.Attribute{
   610  					"id":       {Type: cty.String, Optional: true, Computed: true},
   611  					"saml:aud": {Type: cty.String, Optional: true},
   612  					"zeta":     {Type: cty.String, Optional: true},
   613  				},
   614  			},
   615  			RequiredReplace: cty.NewPathSet(),
   616  			ExpectedOutput: `  # test_instance.example will be updated in-place
   617    ~ resource "test_instance" "example" {
   618          id         = "i-02ae66f368e8518a9"
   619        ~ "saml:aud" = "https://example.com/saml" -> "https://saml.example.com"
   620          # (1 unchanged attribute hidden)
   621      }`,
   622  		},
   623  		"string force-new update": {
   624  			Action:       plans.DeleteThenCreate,
   625  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
   626  			Mode:         addrs.ManagedResourceMode,
   627  			Before: cty.ObjectVal(map[string]cty.Value{
   628  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   629  				"ami": cty.StringVal("ami-BEFORE"),
   630  			}),
   631  			After: cty.ObjectVal(map[string]cty.Value{
   632  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   633  				"ami": cty.StringVal("ami-AFTER"),
   634  			}),
   635  			Schema: &configschema.Block{
   636  				Attributes: map[string]*configschema.Attribute{
   637  					"id":  {Type: cty.String, Optional: true, Computed: true},
   638  					"ami": {Type: cty.String, Optional: true},
   639  				},
   640  			},
   641  			RequiredReplace: cty.NewPathSet(cty.Path{
   642  				cty.GetAttrStep{Name: "ami"},
   643  			}),
   644  			ExpectedOutput: `  # test_instance.example must be replaced
   645  -/+ resource "test_instance" "example" {
   646        ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement
   647          id  = "i-02ae66f368e8518a9"
   648      }`,
   649  		},
   650  		"string in-place update (null values)": {
   651  			Action: plans.Update,
   652  			Mode:   addrs.ManagedResourceMode,
   653  			Before: cty.ObjectVal(map[string]cty.Value{
   654  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
   655  				"ami":       cty.StringVal("ami-BEFORE"),
   656  				"unchanged": cty.NullVal(cty.String),
   657  			}),
   658  			After: cty.ObjectVal(map[string]cty.Value{
   659  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
   660  				"ami":       cty.StringVal("ami-AFTER"),
   661  				"unchanged": cty.NullVal(cty.String),
   662  			}),
   663  			Schema: &configschema.Block{
   664  				Attributes: map[string]*configschema.Attribute{
   665  					"id":        {Type: cty.String, Optional: true, Computed: true},
   666  					"ami":       {Type: cty.String, Optional: true},
   667  					"unchanged": {Type: cty.String, Optional: true},
   668  				},
   669  			},
   670  			RequiredReplace: cty.NewPathSet(),
   671  			ExpectedOutput: `  # test_instance.example will be updated in-place
   672    ~ resource "test_instance" "example" {
   673        ~ ami = "ami-BEFORE" -> "ami-AFTER"
   674          id  = "i-02ae66f368e8518a9"
   675      }`,
   676  		},
   677  		"in-place update of multi-line string field": {
   678  			Action: plans.Update,
   679  			Mode:   addrs.ManagedResourceMode,
   680  			Before: cty.ObjectVal(map[string]cty.Value{
   681  				"id": cty.StringVal("i-02ae66f368e8518a9"),
   682  				"more_lines": cty.StringVal(`original
   683  long
   684  multi-line
   685  string
   686  field`),
   687  			}),
   688  			After: cty.ObjectVal(map[string]cty.Value{
   689  				"id": cty.UnknownVal(cty.String),
   690  				"more_lines": cty.StringVal(`original
   691  extremely long
   692  multi-line
   693  string
   694  field`),
   695  			}),
   696  			Schema: &configschema.Block{
   697  				Attributes: map[string]*configschema.Attribute{
   698  					"id":         {Type: cty.String, Optional: true, Computed: true},
   699  					"more_lines": {Type: cty.String, Optional: true},
   700  				},
   701  			},
   702  			RequiredReplace: cty.NewPathSet(),
   703  			ExpectedOutput: `  # test_instance.example will be updated in-place
   704    ~ resource "test_instance" "example" {
   705        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   706        ~ more_lines = <<-EOT
   707              original
   708            - long
   709            + extremely long
   710              multi-line
   711              string
   712              field
   713          EOT
   714      }`,
   715  		},
   716  		"addition of multi-line string field": {
   717  			Action: plans.Update,
   718  			Mode:   addrs.ManagedResourceMode,
   719  			Before: cty.ObjectVal(map[string]cty.Value{
   720  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   721  				"more_lines": cty.NullVal(cty.String),
   722  			}),
   723  			After: cty.ObjectVal(map[string]cty.Value{
   724  				"id": cty.UnknownVal(cty.String),
   725  				"more_lines": cty.StringVal(`original
   726  new line`),
   727  			}),
   728  			Schema: &configschema.Block{
   729  				Attributes: map[string]*configschema.Attribute{
   730  					"id":         {Type: cty.String, Optional: true, Computed: true},
   731  					"more_lines": {Type: cty.String, Optional: true},
   732  				},
   733  			},
   734  			RequiredReplace: cty.NewPathSet(),
   735  			ExpectedOutput: `  # test_instance.example will be updated in-place
   736    ~ resource "test_instance" "example" {
   737        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   738        + more_lines = <<-EOT
   739              original
   740              new line
   741          EOT
   742      }`,
   743  		},
   744  		"force-new update of multi-line string field": {
   745  			Action: plans.DeleteThenCreate,
   746  			Mode:   addrs.ManagedResourceMode,
   747  			Before: cty.ObjectVal(map[string]cty.Value{
   748  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   749  				"more_lines": cty.StringVal(`original`),
   750  			}),
   751  			After: cty.ObjectVal(map[string]cty.Value{
   752  				"id": cty.UnknownVal(cty.String),
   753  				"more_lines": cty.StringVal(`original
   754  new line`),
   755  			}),
   756  			Schema: &configschema.Block{
   757  				Attributes: map[string]*configschema.Attribute{
   758  					"id":         {Type: cty.String, Optional: true, Computed: true},
   759  					"more_lines": {Type: cty.String, Optional: true},
   760  				},
   761  			},
   762  			RequiredReplace: cty.NewPathSet(cty.Path{
   763  				cty.GetAttrStep{Name: "more_lines"},
   764  			}),
   765  			ExpectedOutput: `  # test_instance.example must be replaced
   766  -/+ resource "test_instance" "example" {
   767        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   768        ~ more_lines = <<-EOT # forces replacement
   769              original
   770            + new line
   771          EOT
   772      }`,
   773  		},
   774  
   775  		// Sensitive
   776  
   777  		"creation with sensitive field": {
   778  			Action: plans.Create,
   779  			Mode:   addrs.ManagedResourceMode,
   780  			Before: cty.NullVal(cty.EmptyObject),
   781  			After: cty.ObjectVal(map[string]cty.Value{
   782  				"id":       cty.UnknownVal(cty.String),
   783  				"password": cty.StringVal("top-secret"),
   784  				"conn_info": cty.ObjectVal(map[string]cty.Value{
   785  					"user":     cty.StringVal("not-secret"),
   786  					"password": cty.StringVal("top-secret"),
   787  				}),
   788  			}),
   789  			Schema: &configschema.Block{
   790  				Attributes: map[string]*configschema.Attribute{
   791  					"id":       {Type: cty.String, Computed: true},
   792  					"password": {Type: cty.String, Optional: true, Sensitive: true},
   793  					"conn_info": {
   794  						NestedType: &configschema.Object{
   795  							Nesting: configschema.NestingSingle,
   796  							Attributes: map[string]*configschema.Attribute{
   797  								"user":     {Type: cty.String, Optional: true},
   798  								"password": {Type: cty.String, Optional: true, Sensitive: true},
   799  							},
   800  						},
   801  					},
   802  				},
   803  			},
   804  			RequiredReplace: cty.NewPathSet(),
   805  			ExpectedOutput: `  # test_instance.example will be created
   806    + resource "test_instance" "example" {
   807        + conn_info = {
   808            + password = (sensitive value)
   809            + user     = "not-secret"
   810          }
   811        + id        = (known after apply)
   812        + password  = (sensitive value)
   813      }`,
   814  		},
   815  		"update with equal sensitive field": {
   816  			Action: plans.Update,
   817  			Mode:   addrs.ManagedResourceMode,
   818  			Before: cty.ObjectVal(map[string]cty.Value{
   819  				"id":       cty.StringVal("blah"),
   820  				"str":      cty.StringVal("before"),
   821  				"password": cty.StringVal("top-secret"),
   822  			}),
   823  			After: cty.ObjectVal(map[string]cty.Value{
   824  				"id":       cty.UnknownVal(cty.String),
   825  				"str":      cty.StringVal("after"),
   826  				"password": cty.StringVal("top-secret"),
   827  			}),
   828  			Schema: &configschema.Block{
   829  				Attributes: map[string]*configschema.Attribute{
   830  					"id":       {Type: cty.String, Computed: true},
   831  					"str":      {Type: cty.String, Optional: true},
   832  					"password": {Type: cty.String, Optional: true, Sensitive: true},
   833  				},
   834  			},
   835  			RequiredReplace: cty.NewPathSet(),
   836  			ExpectedOutput: `  # test_instance.example will be updated in-place
   837    ~ resource "test_instance" "example" {
   838        ~ id       = "blah" -> (known after apply)
   839        ~ str      = "before" -> "after"
   840          # (1 unchanged attribute hidden)
   841      }`,
   842  		},
   843  
   844  		// tainted objects
   845  		"replace tainted resource": {
   846  			Action:       plans.DeleteThenCreate,
   847  			ActionReason: plans.ResourceInstanceReplaceBecauseTainted,
   848  			Mode:         addrs.ManagedResourceMode,
   849  			Before: cty.ObjectVal(map[string]cty.Value{
   850  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   851  				"ami": cty.StringVal("ami-BEFORE"),
   852  			}),
   853  			After: cty.ObjectVal(map[string]cty.Value{
   854  				"id":  cty.UnknownVal(cty.String),
   855  				"ami": cty.StringVal("ami-AFTER"),
   856  			}),
   857  			Schema: &configschema.Block{
   858  				Attributes: map[string]*configschema.Attribute{
   859  					"id":  {Type: cty.String, Optional: true, Computed: true},
   860  					"ami": {Type: cty.String, Optional: true},
   861  				},
   862  			},
   863  			RequiredReplace: cty.NewPathSet(cty.Path{
   864  				cty.GetAttrStep{Name: "ami"},
   865  			}),
   866  			ExpectedOutput: `  # test_instance.example is tainted, so must be replaced
   867  -/+ resource "test_instance" "example" {
   868        ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement
   869        ~ id  = "i-02ae66f368e8518a9" -> (known after apply)
   870      }`,
   871  		},
   872  		"force replacement with empty before value": {
   873  			Action:       plans.DeleteThenCreate,
   874  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
   875  			Mode:         addrs.ManagedResourceMode,
   876  			Before: cty.ObjectVal(map[string]cty.Value{
   877  				"name":   cty.StringVal("name"),
   878  				"forced": cty.NullVal(cty.String),
   879  			}),
   880  			After: cty.ObjectVal(map[string]cty.Value{
   881  				"name":   cty.StringVal("name"),
   882  				"forced": cty.StringVal("example"),
   883  			}),
   884  			Schema: &configschema.Block{
   885  				Attributes: map[string]*configschema.Attribute{
   886  					"name":   {Type: cty.String, Optional: true},
   887  					"forced": {Type: cty.String, Optional: true},
   888  				},
   889  			},
   890  			RequiredReplace: cty.NewPathSet(cty.Path{
   891  				cty.GetAttrStep{Name: "forced"},
   892  			}),
   893  			ExpectedOutput: `  # test_instance.example must be replaced
   894  -/+ resource "test_instance" "example" {
   895        + forced = "example" # forces replacement
   896          name   = "name"
   897      }`,
   898  		},
   899  		"force replacement with empty before value legacy": {
   900  			Action:       plans.DeleteThenCreate,
   901  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
   902  			Mode:         addrs.ManagedResourceMode,
   903  			Before: cty.ObjectVal(map[string]cty.Value{
   904  				"name":   cty.StringVal("name"),
   905  				"forced": cty.StringVal(""),
   906  			}),
   907  			After: cty.ObjectVal(map[string]cty.Value{
   908  				"name":   cty.StringVal("name"),
   909  				"forced": cty.StringVal("example"),
   910  			}),
   911  			Schema: &configschema.Block{
   912  				Attributes: map[string]*configschema.Attribute{
   913  					"name":   {Type: cty.String, Optional: true},
   914  					"forced": {Type: cty.String, Optional: true},
   915  				},
   916  			},
   917  			RequiredReplace: cty.NewPathSet(cty.Path{
   918  				cty.GetAttrStep{Name: "forced"},
   919  			}),
   920  			ExpectedOutput: `  # test_instance.example must be replaced
   921  -/+ resource "test_instance" "example" {
   922        + forced = "example" # forces replacement
   923          name   = "name"
   924      }`,
   925  		},
   926  		"read during apply because of unknown configuration": {
   927  			Action:       plans.Read,
   928  			ActionReason: plans.ResourceInstanceReadBecauseConfigUnknown,
   929  			Mode:         addrs.DataResourceMode,
   930  			Before: cty.ObjectVal(map[string]cty.Value{
   931  				"name": cty.StringVal("name"),
   932  			}),
   933  			After: cty.ObjectVal(map[string]cty.Value{
   934  				"name": cty.StringVal("name"),
   935  			}),
   936  			Schema: &configschema.Block{
   937  				Attributes: map[string]*configschema.Attribute{
   938  					"name": {Type: cty.String, Optional: true},
   939  				},
   940  			},
   941  			ExpectedOutput: `  # data.test_instance.example will be read during apply
   942    # (config refers to values not yet known)
   943   <= data "test_instance" "example" {
   944          name = "name"
   945      }`,
   946  		},
   947  		"read during apply because of pending changes to upstream dependency": {
   948  			Action:       plans.Read,
   949  			ActionReason: plans.ResourceInstanceReadBecauseDependencyPending,
   950  			Mode:         addrs.DataResourceMode,
   951  			Before: cty.ObjectVal(map[string]cty.Value{
   952  				"name": cty.StringVal("name"),
   953  			}),
   954  			After: cty.ObjectVal(map[string]cty.Value{
   955  				"name": cty.StringVal("name"),
   956  			}),
   957  			Schema: &configschema.Block{
   958  				Attributes: map[string]*configschema.Attribute{
   959  					"name": {Type: cty.String, Optional: true},
   960  				},
   961  			},
   962  			ExpectedOutput: `  # data.test_instance.example will be read during apply
   963    # (depends on a resource or a module with changes pending)
   964   <= data "test_instance" "example" {
   965          name = "name"
   966      }`,
   967  		},
   968  		"read during apply for unspecified reason": {
   969  			Action: plans.Read,
   970  			Mode:   addrs.DataResourceMode,
   971  			Before: cty.ObjectVal(map[string]cty.Value{
   972  				"name": cty.StringVal("name"),
   973  			}),
   974  			After: cty.ObjectVal(map[string]cty.Value{
   975  				"name": cty.StringVal("name"),
   976  			}),
   977  			Schema: &configschema.Block{
   978  				Attributes: map[string]*configschema.Attribute{
   979  					"name": {Type: cty.String, Optional: true},
   980  				},
   981  			},
   982  			ExpectedOutput: `  # data.test_instance.example will be read during apply
   983   <= data "test_instance" "example" {
   984          name = "name"
   985      }`,
   986  		},
   987  		"show all identifying attributes even if unchanged": {
   988  			Action: plans.Update,
   989  			Mode:   addrs.ManagedResourceMode,
   990  			Before: cty.ObjectVal(map[string]cty.Value{
   991  				"id":   cty.StringVal("i-02ae66f368e8518a9"),
   992  				"ami":  cty.StringVal("ami-BEFORE"),
   993  				"bar":  cty.StringVal("bar"),
   994  				"foo":  cty.StringVal("foo"),
   995  				"name": cty.StringVal("alice"),
   996  				"tags": cty.MapVal(map[string]cty.Value{
   997  					"name": cty.StringVal("bob"),
   998  				}),
   999  			}),
  1000  			After: cty.ObjectVal(map[string]cty.Value{
  1001  				"id":   cty.StringVal("i-02ae66f368e8518a9"),
  1002  				"ami":  cty.StringVal("ami-AFTER"),
  1003  				"bar":  cty.StringVal("bar"),
  1004  				"foo":  cty.StringVal("foo"),
  1005  				"name": cty.StringVal("alice"),
  1006  				"tags": cty.MapVal(map[string]cty.Value{
  1007  					"name": cty.StringVal("bob"),
  1008  				}),
  1009  			}),
  1010  			Schema: &configschema.Block{
  1011  				Attributes: map[string]*configschema.Attribute{
  1012  					"id":   {Type: cty.String, Optional: true, Computed: true},
  1013  					"ami":  {Type: cty.String, Optional: true},
  1014  					"bar":  {Type: cty.String, Optional: true},
  1015  					"foo":  {Type: cty.String, Optional: true},
  1016  					"name": {Type: cty.String, Optional: true},
  1017  					"tags": {Type: cty.Map(cty.String), Optional: true},
  1018  				},
  1019  			},
  1020  			RequiredReplace: cty.NewPathSet(),
  1021  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1022    ~ resource "test_instance" "example" {
  1023        ~ ami  = "ami-BEFORE" -> "ami-AFTER"
  1024          id   = "i-02ae66f368e8518a9"
  1025          name = "alice"
  1026          tags = {
  1027              "name" = "bob"
  1028          }
  1029          # (2 unchanged attributes hidden)
  1030      }`,
  1031  		},
  1032  	}
  1033  
  1034  	runTestCases(t, testCases)
  1035  }
  1036  
  1037  func TestResourceChange_JSON(t *testing.T) {
  1038  	testCases := map[string]testCase{
  1039  		"creation": {
  1040  			Action: plans.Create,
  1041  			Mode:   addrs.ManagedResourceMode,
  1042  			Before: cty.NullVal(cty.EmptyObject),
  1043  			After: cty.ObjectVal(map[string]cty.Value{
  1044  				"id": cty.UnknownVal(cty.String),
  1045  				"json_field": cty.StringVal(`{
  1046  					"str": "value",
  1047  					"list":["a","b", 234, true],
  1048  					"obj": {"key": "val"}
  1049  				}`),
  1050  			}),
  1051  			Schema: &configschema.Block{
  1052  				Attributes: map[string]*configschema.Attribute{
  1053  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1054  					"json_field": {Type: cty.String, Optional: true},
  1055  				},
  1056  			},
  1057  			RequiredReplace: cty.NewPathSet(),
  1058  			ExpectedOutput: `  # test_instance.example will be created
  1059    + resource "test_instance" "example" {
  1060        + id         = (known after apply)
  1061        + json_field = jsonencode(
  1062              {
  1063                + list = [
  1064                    + "a",
  1065                    + "b",
  1066                    + 234,
  1067                    + true,
  1068                  ]
  1069                + obj  = {
  1070                    + key = "val"
  1071                  }
  1072                + str  = "value"
  1073              }
  1074          )
  1075      }`,
  1076  		},
  1077  		"in-place update of object": {
  1078  			Action: plans.Update,
  1079  			Mode:   addrs.ManagedResourceMode,
  1080  			Before: cty.ObjectVal(map[string]cty.Value{
  1081  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1082  				"json_field": cty.StringVal(`{"aaa": "value","ccc": 5}`),
  1083  			}),
  1084  			After: cty.ObjectVal(map[string]cty.Value{
  1085  				"id":         cty.UnknownVal(cty.String),
  1086  				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`),
  1087  			}),
  1088  			Schema: &configschema.Block{
  1089  				Attributes: map[string]*configschema.Attribute{
  1090  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1091  					"json_field": {Type: cty.String, Optional: true},
  1092  				},
  1093  			},
  1094  			RequiredReplace: cty.NewPathSet(),
  1095  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1096    ~ resource "test_instance" "example" {
  1097        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1098        ~ json_field = jsonencode(
  1099            ~ {
  1100                + bbb = "new_value"
  1101                - ccc = 5
  1102                  # (1 unchanged attribute hidden)
  1103              }
  1104          )
  1105      }`,
  1106  		},
  1107  		"in-place update of object with quoted keys": {
  1108  			Action: plans.Update,
  1109  			Mode:   addrs.ManagedResourceMode,
  1110  			Before: cty.ObjectVal(map[string]cty.Value{
  1111  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1112  				"json_field": cty.StringVal(`{"aaa": "value", "c:c": "old_value"}`),
  1113  			}),
  1114  			After: cty.ObjectVal(map[string]cty.Value{
  1115  				"id":         cty.UnknownVal(cty.String),
  1116  				"json_field": cty.StringVal(`{"aaa": "value", "b:bb": "new_value"}`),
  1117  			}),
  1118  			Schema: &configschema.Block{
  1119  				Attributes: map[string]*configschema.Attribute{
  1120  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1121  					"json_field": {Type: cty.String, Optional: true},
  1122  				},
  1123  			},
  1124  			RequiredReplace: cty.NewPathSet(),
  1125  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1126    ~ resource "test_instance" "example" {
  1127        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1128        ~ json_field = jsonencode(
  1129            ~ {
  1130                + "b:bb" = "new_value"
  1131                - "c:c"  = "old_value"
  1132                  # (1 unchanged attribute hidden)
  1133              }
  1134          )
  1135      }`,
  1136  		},
  1137  		"in-place update (from empty tuple)": {
  1138  			Action: plans.Update,
  1139  			Mode:   addrs.ManagedResourceMode,
  1140  			Before: cty.ObjectVal(map[string]cty.Value{
  1141  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1142  				"json_field": cty.StringVal(`{"aaa": []}`),
  1143  			}),
  1144  			After: cty.ObjectVal(map[string]cty.Value{
  1145  				"id":         cty.UnknownVal(cty.String),
  1146  				"json_field": cty.StringVal(`{"aaa": ["value"]}`),
  1147  			}),
  1148  			Schema: &configschema.Block{
  1149  				Attributes: map[string]*configschema.Attribute{
  1150  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1151  					"json_field": {Type: cty.String, Optional: true},
  1152  				},
  1153  			},
  1154  			RequiredReplace: cty.NewPathSet(),
  1155  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1156    ~ resource "test_instance" "example" {
  1157        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1158        ~ json_field = jsonencode(
  1159            ~ {
  1160                ~ aaa = [
  1161                    + "value",
  1162                  ]
  1163              }
  1164          )
  1165      }`,
  1166  		},
  1167  		"in-place update (to empty tuple)": {
  1168  			Action: plans.Update,
  1169  			Mode:   addrs.ManagedResourceMode,
  1170  			Before: cty.ObjectVal(map[string]cty.Value{
  1171  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1172  				"json_field": cty.StringVal(`{"aaa": ["value"]}`),
  1173  			}),
  1174  			After: cty.ObjectVal(map[string]cty.Value{
  1175  				"id":         cty.UnknownVal(cty.String),
  1176  				"json_field": cty.StringVal(`{"aaa": []}`),
  1177  			}),
  1178  			Schema: &configschema.Block{
  1179  				Attributes: map[string]*configschema.Attribute{
  1180  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1181  					"json_field": {Type: cty.String, Optional: true},
  1182  				},
  1183  			},
  1184  			RequiredReplace: cty.NewPathSet(),
  1185  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1186    ~ resource "test_instance" "example" {
  1187        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1188        ~ json_field = jsonencode(
  1189            ~ {
  1190                ~ aaa = [
  1191                    - "value",
  1192                  ]
  1193              }
  1194          )
  1195      }`,
  1196  		},
  1197  		"in-place update (tuple of different types)": {
  1198  			Action: plans.Update,
  1199  			Mode:   addrs.ManagedResourceMode,
  1200  			Before: cty.ObjectVal(map[string]cty.Value{
  1201  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1202  				"json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`),
  1203  			}),
  1204  			After: cty.ObjectVal(map[string]cty.Value{
  1205  				"id":         cty.UnknownVal(cty.String),
  1206  				"json_field": cty.StringVal(`{"aaa": [42, {"foo":"baz"}, "value"]}`),
  1207  			}),
  1208  			Schema: &configschema.Block{
  1209  				Attributes: map[string]*configschema.Attribute{
  1210  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1211  					"json_field": {Type: cty.String, Optional: true},
  1212  				},
  1213  			},
  1214  			RequiredReplace: cty.NewPathSet(),
  1215  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1216    ~ resource "test_instance" "example" {
  1217        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1218        ~ json_field = jsonencode(
  1219            ~ {
  1220                ~ aaa = [
  1221                      42,
  1222                    ~ {
  1223                        ~ foo = "bar" -> "baz"
  1224                      },
  1225                      "value",
  1226                  ]
  1227              }
  1228          )
  1229      }`,
  1230  		},
  1231  		"force-new update": {
  1232  			Action:       plans.DeleteThenCreate,
  1233  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  1234  			Mode:         addrs.ManagedResourceMode,
  1235  			Before: cty.ObjectVal(map[string]cty.Value{
  1236  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1237  				"json_field": cty.StringVal(`{"aaa": "value"}`),
  1238  			}),
  1239  			After: cty.ObjectVal(map[string]cty.Value{
  1240  				"id":         cty.UnknownVal(cty.String),
  1241  				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`),
  1242  			}),
  1243  			Schema: &configschema.Block{
  1244  				Attributes: map[string]*configschema.Attribute{
  1245  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1246  					"json_field": {Type: cty.String, Optional: true},
  1247  				},
  1248  			},
  1249  			RequiredReplace: cty.NewPathSet(cty.Path{
  1250  				cty.GetAttrStep{Name: "json_field"},
  1251  			}),
  1252  			ExpectedOutput: `  # test_instance.example must be replaced
  1253  -/+ resource "test_instance" "example" {
  1254        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1255        ~ json_field = jsonencode(
  1256            ~ {
  1257                + bbb = "new_value"
  1258                  # (1 unchanged attribute hidden)
  1259              } # forces replacement
  1260          )
  1261      }`,
  1262  		},
  1263  		"in-place update (whitespace change)": {
  1264  			Action: plans.Update,
  1265  			Mode:   addrs.ManagedResourceMode,
  1266  			Before: cty.ObjectVal(map[string]cty.Value{
  1267  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1268  				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`),
  1269  			}),
  1270  			After: cty.ObjectVal(map[string]cty.Value{
  1271  				"id": cty.UnknownVal(cty.String),
  1272  				"json_field": cty.StringVal(`{"aaa":"value",
  1273  					"bbb":"another"}`),
  1274  			}),
  1275  			Schema: &configschema.Block{
  1276  				Attributes: map[string]*configschema.Attribute{
  1277  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1278  					"json_field": {Type: cty.String, Optional: true},
  1279  				},
  1280  			},
  1281  			RequiredReplace: cty.NewPathSet(),
  1282  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1283    ~ resource "test_instance" "example" {
  1284        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1285        ~ json_field = jsonencode( # whitespace changes
  1286              {
  1287                  aaa = "value"
  1288                  bbb = "another"
  1289              }
  1290          )
  1291      }`,
  1292  		},
  1293  		"force-new update (whitespace change)": {
  1294  			Action:       plans.DeleteThenCreate,
  1295  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  1296  			Mode:         addrs.ManagedResourceMode,
  1297  			Before: cty.ObjectVal(map[string]cty.Value{
  1298  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1299  				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`),
  1300  			}),
  1301  			After: cty.ObjectVal(map[string]cty.Value{
  1302  				"id": cty.UnknownVal(cty.String),
  1303  				"json_field": cty.StringVal(`{"aaa":"value",
  1304  					"bbb":"another"}`),
  1305  			}),
  1306  			Schema: &configschema.Block{
  1307  				Attributes: map[string]*configschema.Attribute{
  1308  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1309  					"json_field": {Type: cty.String, Optional: true},
  1310  				},
  1311  			},
  1312  			RequiredReplace: cty.NewPathSet(cty.Path{
  1313  				cty.GetAttrStep{Name: "json_field"},
  1314  			}),
  1315  			ExpectedOutput: `  # test_instance.example must be replaced
  1316  -/+ resource "test_instance" "example" {
  1317        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1318        ~ json_field = jsonencode( # whitespace changes force replacement
  1319              {
  1320                  aaa = "value"
  1321                  bbb = "another"
  1322              }
  1323          )
  1324      }`,
  1325  		},
  1326  		"creation (empty)": {
  1327  			Action: plans.Create,
  1328  			Mode:   addrs.ManagedResourceMode,
  1329  			Before: cty.NullVal(cty.EmptyObject),
  1330  			After: cty.ObjectVal(map[string]cty.Value{
  1331  				"id":         cty.UnknownVal(cty.String),
  1332  				"json_field": cty.StringVal(`{}`),
  1333  			}),
  1334  			Schema: &configschema.Block{
  1335  				Attributes: map[string]*configschema.Attribute{
  1336  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1337  					"json_field": {Type: cty.String, Optional: true},
  1338  				},
  1339  			},
  1340  			RequiredReplace: cty.NewPathSet(),
  1341  			ExpectedOutput: `  # test_instance.example will be created
  1342    + resource "test_instance" "example" {
  1343        + id         = (known after apply)
  1344        + json_field = jsonencode({})
  1345      }`,
  1346  		},
  1347  		"JSON list item removal": {
  1348  			Action: plans.Update,
  1349  			Mode:   addrs.ManagedResourceMode,
  1350  			Before: cty.ObjectVal(map[string]cty.Value{
  1351  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1352  				"json_field": cty.StringVal(`["first","second","third"]`),
  1353  			}),
  1354  			After: cty.ObjectVal(map[string]cty.Value{
  1355  				"id":         cty.UnknownVal(cty.String),
  1356  				"json_field": cty.StringVal(`["first","second"]`),
  1357  			}),
  1358  			Schema: &configschema.Block{
  1359  				Attributes: map[string]*configschema.Attribute{
  1360  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1361  					"json_field": {Type: cty.String, Optional: true},
  1362  				},
  1363  			},
  1364  			RequiredReplace: cty.NewPathSet(),
  1365  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1366    ~ resource "test_instance" "example" {
  1367        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1368        ~ json_field = jsonencode(
  1369            ~ [
  1370                  # (1 unchanged element hidden)
  1371                  "second",
  1372                - "third",
  1373              ]
  1374          )
  1375      }`,
  1376  		},
  1377  		"JSON list item addition": {
  1378  			Action: plans.Update,
  1379  			Mode:   addrs.ManagedResourceMode,
  1380  			Before: cty.ObjectVal(map[string]cty.Value{
  1381  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1382  				"json_field": cty.StringVal(`["first","second"]`),
  1383  			}),
  1384  			After: cty.ObjectVal(map[string]cty.Value{
  1385  				"id":         cty.UnknownVal(cty.String),
  1386  				"json_field": cty.StringVal(`["first","second","third"]`),
  1387  			}),
  1388  			Schema: &configschema.Block{
  1389  				Attributes: map[string]*configschema.Attribute{
  1390  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1391  					"json_field": {Type: cty.String, Optional: true},
  1392  				},
  1393  			},
  1394  			RequiredReplace: cty.NewPathSet(),
  1395  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1396    ~ resource "test_instance" "example" {
  1397        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1398        ~ json_field = jsonencode(
  1399            ~ [
  1400                  # (1 unchanged element hidden)
  1401                  "second",
  1402                + "third",
  1403              ]
  1404          )
  1405      }`,
  1406  		},
  1407  		"JSON list object addition": {
  1408  			Action: plans.Update,
  1409  			Mode:   addrs.ManagedResourceMode,
  1410  			Before: cty.ObjectVal(map[string]cty.Value{
  1411  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1412  				"json_field": cty.StringVal(`{"first":"111"}`),
  1413  			}),
  1414  			After: cty.ObjectVal(map[string]cty.Value{
  1415  				"id":         cty.UnknownVal(cty.String),
  1416  				"json_field": cty.StringVal(`{"first":"111","second":"222"}`),
  1417  			}),
  1418  			Schema: &configschema.Block{
  1419  				Attributes: map[string]*configschema.Attribute{
  1420  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1421  					"json_field": {Type: cty.String, Optional: true},
  1422  				},
  1423  			},
  1424  			RequiredReplace: cty.NewPathSet(),
  1425  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1426    ~ resource "test_instance" "example" {
  1427        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1428        ~ json_field = jsonencode(
  1429            ~ {
  1430                + second = "222"
  1431                  # (1 unchanged attribute hidden)
  1432              }
  1433          )
  1434      }`,
  1435  		},
  1436  		"JSON object with nested list": {
  1437  			Action: plans.Update,
  1438  			Mode:   addrs.ManagedResourceMode,
  1439  			Before: cty.ObjectVal(map[string]cty.Value{
  1440  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  1441  				"json_field": cty.StringVal(`{
  1442  		  "Statement": ["first"]
  1443  		}`),
  1444  			}),
  1445  			After: cty.ObjectVal(map[string]cty.Value{
  1446  				"id": cty.UnknownVal(cty.String),
  1447  				"json_field": cty.StringVal(`{
  1448  		  "Statement": ["first", "second"]
  1449  		}`),
  1450  			}),
  1451  			Schema: &configschema.Block{
  1452  				Attributes: map[string]*configschema.Attribute{
  1453  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1454  					"json_field": {Type: cty.String, Optional: true},
  1455  				},
  1456  			},
  1457  			RequiredReplace: cty.NewPathSet(),
  1458  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1459    ~ resource "test_instance" "example" {
  1460        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1461        ~ json_field = jsonencode(
  1462            ~ {
  1463                ~ Statement = [
  1464                      "first",
  1465                    + "second",
  1466                  ]
  1467              }
  1468          )
  1469      }`,
  1470  		},
  1471  		"JSON list of objects - adding item": {
  1472  			Action: plans.Update,
  1473  			Mode:   addrs.ManagedResourceMode,
  1474  			Before: cty.ObjectVal(map[string]cty.Value{
  1475  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1476  				"json_field": cty.StringVal(`[{"one": "111"}]`),
  1477  			}),
  1478  			After: cty.ObjectVal(map[string]cty.Value{
  1479  				"id":         cty.UnknownVal(cty.String),
  1480  				"json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}]`),
  1481  			}),
  1482  			Schema: &configschema.Block{
  1483  				Attributes: map[string]*configschema.Attribute{
  1484  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1485  					"json_field": {Type: cty.String, Optional: true},
  1486  				},
  1487  			},
  1488  			RequiredReplace: cty.NewPathSet(),
  1489  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1490    ~ resource "test_instance" "example" {
  1491        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1492        ~ json_field = jsonencode(
  1493            ~ [
  1494                  {
  1495                      one = "111"
  1496                  },
  1497                + {
  1498                    + two = "222"
  1499                  },
  1500              ]
  1501          )
  1502      }`,
  1503  		},
  1504  		"JSON list of objects - removing item": {
  1505  			Action: plans.Update,
  1506  			Mode:   addrs.ManagedResourceMode,
  1507  			Before: cty.ObjectVal(map[string]cty.Value{
  1508  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1509  				"json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}, {"three": "333"}]`),
  1510  			}),
  1511  			After: cty.ObjectVal(map[string]cty.Value{
  1512  				"id":         cty.UnknownVal(cty.String),
  1513  				"json_field": cty.StringVal(`[{"one": "111"}, {"three": "333"}]`),
  1514  			}),
  1515  			Schema: &configschema.Block{
  1516  				Attributes: map[string]*configschema.Attribute{
  1517  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1518  					"json_field": {Type: cty.String, Optional: true},
  1519  				},
  1520  			},
  1521  			RequiredReplace: cty.NewPathSet(),
  1522  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1523    ~ resource "test_instance" "example" {
  1524        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1525        ~ json_field = jsonencode(
  1526            ~ [
  1527                  {
  1528                      one = "111"
  1529                  },
  1530                - {
  1531                    - two = "222"
  1532                  },
  1533                  {
  1534                      three = "333"
  1535                  },
  1536              ]
  1537          )
  1538      }`,
  1539  		},
  1540  		"JSON object with list of objects": {
  1541  			Action: plans.Update,
  1542  			Mode:   addrs.ManagedResourceMode,
  1543  			Before: cty.ObjectVal(map[string]cty.Value{
  1544  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1545  				"json_field": cty.StringVal(`{"parent":[{"one": "111"}]}`),
  1546  			}),
  1547  			After: cty.ObjectVal(map[string]cty.Value{
  1548  				"id":         cty.UnknownVal(cty.String),
  1549  				"json_field": cty.StringVal(`{"parent":[{"one": "111"}, {"two": "222"}]}`),
  1550  			}),
  1551  			Schema: &configschema.Block{
  1552  				Attributes: map[string]*configschema.Attribute{
  1553  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1554  					"json_field": {Type: cty.String, Optional: true},
  1555  				},
  1556  			},
  1557  			RequiredReplace: cty.NewPathSet(),
  1558  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1559    ~ resource "test_instance" "example" {
  1560        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1561        ~ json_field = jsonencode(
  1562            ~ {
  1563                ~ parent = [
  1564                      {
  1565                          one = "111"
  1566                      },
  1567                    + {
  1568                        + two = "222"
  1569                      },
  1570                  ]
  1571              }
  1572          )
  1573      }`,
  1574  		},
  1575  		"JSON object double nested lists": {
  1576  			Action: plans.Update,
  1577  			Mode:   addrs.ManagedResourceMode,
  1578  			Before: cty.ObjectVal(map[string]cty.Value{
  1579  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1580  				"json_field": cty.StringVal(`{"parent":[{"another_list": ["111"]}]}`),
  1581  			}),
  1582  			After: cty.ObjectVal(map[string]cty.Value{
  1583  				"id":         cty.UnknownVal(cty.String),
  1584  				"json_field": cty.StringVal(`{"parent":[{"another_list": ["111", "222"]}]}`),
  1585  			}),
  1586  			Schema: &configschema.Block{
  1587  				Attributes: map[string]*configschema.Attribute{
  1588  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1589  					"json_field": {Type: cty.String, Optional: true},
  1590  				},
  1591  			},
  1592  			RequiredReplace: cty.NewPathSet(),
  1593  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1594    ~ resource "test_instance" "example" {
  1595        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1596        ~ json_field = jsonencode(
  1597            ~ {
  1598                ~ parent = [
  1599                    ~ {
  1600                        ~ another_list = [
  1601                              "111",
  1602                            + "222",
  1603                          ]
  1604                      },
  1605                  ]
  1606              }
  1607          )
  1608      }`,
  1609  		},
  1610  		"in-place update from object to tuple": {
  1611  			Action: plans.Update,
  1612  			Mode:   addrs.ManagedResourceMode,
  1613  			Before: cty.ObjectVal(map[string]cty.Value{
  1614  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1615  				"json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`),
  1616  			}),
  1617  			After: cty.ObjectVal(map[string]cty.Value{
  1618  				"id":         cty.UnknownVal(cty.String),
  1619  				"json_field": cty.StringVal(`["aaa", 42, "something"]`),
  1620  			}),
  1621  			Schema: &configschema.Block{
  1622  				Attributes: map[string]*configschema.Attribute{
  1623  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1624  					"json_field": {Type: cty.String, Optional: true},
  1625  				},
  1626  			},
  1627  			RequiredReplace: cty.NewPathSet(),
  1628  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1629    ~ resource "test_instance" "example" {
  1630        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1631        ~ json_field = jsonencode(
  1632            ~ {
  1633                - aaa = [
  1634                    - 42,
  1635                    - {
  1636                        - foo = "bar"
  1637                      },
  1638                    - "value",
  1639                  ]
  1640              } -> [
  1641                + "aaa",
  1642                + 42,
  1643                + "something",
  1644              ]
  1645          )
  1646      }`,
  1647  		},
  1648  	}
  1649  	runTestCases(t, testCases)
  1650  }
  1651  
  1652  func TestResourceChange_listObject(t *testing.T) {
  1653  	testCases := map[string]testCase{
  1654  		// https://github.com/terramate-io/tf/issues/30641
  1655  		"updating non-identifying attribute": {
  1656  			Action: plans.Update,
  1657  			Mode:   addrs.ManagedResourceMode,
  1658  			Before: cty.ObjectVal(map[string]cty.Value{
  1659  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  1660  				"accounts": cty.ListVal([]cty.Value{
  1661  					cty.ObjectVal(map[string]cty.Value{
  1662  						"id":     cty.StringVal("1"),
  1663  						"name":   cty.StringVal("production"),
  1664  						"status": cty.StringVal("ACTIVE"),
  1665  					}),
  1666  					cty.ObjectVal(map[string]cty.Value{
  1667  						"id":     cty.StringVal("2"),
  1668  						"name":   cty.StringVal("staging"),
  1669  						"status": cty.StringVal("ACTIVE"),
  1670  					}),
  1671  					cty.ObjectVal(map[string]cty.Value{
  1672  						"id":     cty.StringVal("3"),
  1673  						"name":   cty.StringVal("disaster-recovery"),
  1674  						"status": cty.StringVal("ACTIVE"),
  1675  					}),
  1676  				}),
  1677  			}),
  1678  			After: cty.ObjectVal(map[string]cty.Value{
  1679  				"id": cty.UnknownVal(cty.String),
  1680  				"accounts": cty.ListVal([]cty.Value{
  1681  					cty.ObjectVal(map[string]cty.Value{
  1682  						"id":     cty.StringVal("1"),
  1683  						"name":   cty.StringVal("production"),
  1684  						"status": cty.StringVal("ACTIVE"),
  1685  					}),
  1686  					cty.ObjectVal(map[string]cty.Value{
  1687  						"id":     cty.StringVal("2"),
  1688  						"name":   cty.StringVal("staging"),
  1689  						"status": cty.StringVal("EXPLODED"),
  1690  					}),
  1691  					cty.ObjectVal(map[string]cty.Value{
  1692  						"id":     cty.StringVal("3"),
  1693  						"name":   cty.StringVal("disaster-recovery"),
  1694  						"status": cty.StringVal("ACTIVE"),
  1695  					}),
  1696  				}),
  1697  			}),
  1698  			Schema: &configschema.Block{
  1699  				Attributes: map[string]*configschema.Attribute{
  1700  					"id": {Type: cty.String, Optional: true, Computed: true},
  1701  					"accounts": {
  1702  						Type: cty.List(cty.Object(map[string]cty.Type{
  1703  							"id":     cty.String,
  1704  							"name":   cty.String,
  1705  							"status": cty.String,
  1706  						})),
  1707  					},
  1708  				},
  1709  			},
  1710  			RequiredReplace: cty.NewPathSet(),
  1711  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1712    ~ resource "test_instance" "example" {
  1713        ~ accounts = [
  1714              {
  1715                  id     = "1"
  1716                  name   = "production"
  1717                  status = "ACTIVE"
  1718              },
  1719            ~ {
  1720                  id     = "2"
  1721                  name   = "staging"
  1722                ~ status = "ACTIVE" -> "EXPLODED"
  1723              },
  1724              {
  1725                  id     = "3"
  1726                  name   = "disaster-recovery"
  1727                  status = "ACTIVE"
  1728              },
  1729          ]
  1730        ~ id       = "i-02ae66f368e8518a9" -> (known after apply)
  1731      }`,
  1732  		},
  1733  	}
  1734  	runTestCases(t, testCases)
  1735  }
  1736  
  1737  func TestResourceChange_primitiveList(t *testing.T) {
  1738  	testCases := map[string]testCase{
  1739  		"in-place update - creation": {
  1740  			Action: plans.Update,
  1741  			Mode:   addrs.ManagedResourceMode,
  1742  			Before: cty.ObjectVal(map[string]cty.Value{
  1743  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1744  				"ami":        cty.StringVal("ami-STATIC"),
  1745  				"list_field": cty.NullVal(cty.List(cty.String)),
  1746  			}),
  1747  			After: cty.ObjectVal(map[string]cty.Value{
  1748  				"id":  cty.UnknownVal(cty.String),
  1749  				"ami": cty.StringVal("ami-STATIC"),
  1750  				"list_field": cty.ListVal([]cty.Value{
  1751  					cty.StringVal("new-element"),
  1752  				}),
  1753  			}),
  1754  			Schema: &configschema.Block{
  1755  				Attributes: map[string]*configschema.Attribute{
  1756  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1757  					"ami":        {Type: cty.String, Optional: true},
  1758  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1759  				},
  1760  			},
  1761  			RequiredReplace: cty.NewPathSet(),
  1762  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1763    ~ resource "test_instance" "example" {
  1764        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1765        + list_field = [
  1766            + "new-element",
  1767          ]
  1768          # (1 unchanged attribute hidden)
  1769      }`,
  1770  		},
  1771  		"in-place update - first addition": {
  1772  			Action: plans.Update,
  1773  			Mode:   addrs.ManagedResourceMode,
  1774  			Before: cty.ObjectVal(map[string]cty.Value{
  1775  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1776  				"ami":        cty.StringVal("ami-STATIC"),
  1777  				"list_field": cty.ListValEmpty(cty.String),
  1778  			}),
  1779  			After: cty.ObjectVal(map[string]cty.Value{
  1780  				"id":  cty.UnknownVal(cty.String),
  1781  				"ami": cty.StringVal("ami-STATIC"),
  1782  				"list_field": cty.ListVal([]cty.Value{
  1783  					cty.StringVal("new-element"),
  1784  				}),
  1785  			}),
  1786  			Schema: &configschema.Block{
  1787  				Attributes: map[string]*configschema.Attribute{
  1788  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1789  					"ami":        {Type: cty.String, Optional: true},
  1790  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1791  				},
  1792  			},
  1793  			RequiredReplace: cty.NewPathSet(),
  1794  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1795    ~ resource "test_instance" "example" {
  1796        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1797        ~ list_field = [
  1798            + "new-element",
  1799          ]
  1800          # (1 unchanged attribute hidden)
  1801      }`,
  1802  		},
  1803  		"in-place update - insertion": {
  1804  			Action: plans.Update,
  1805  			Mode:   addrs.ManagedResourceMode,
  1806  			Before: cty.ObjectVal(map[string]cty.Value{
  1807  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1808  				"ami": cty.StringVal("ami-STATIC"),
  1809  				"list_field": cty.ListVal([]cty.Value{
  1810  					cty.StringVal("aaaa"),
  1811  					cty.StringVal("bbbb"),
  1812  					cty.StringVal("dddd"),
  1813  					cty.StringVal("eeee"),
  1814  					cty.StringVal("ffff"),
  1815  				}),
  1816  			}),
  1817  			After: cty.ObjectVal(map[string]cty.Value{
  1818  				"id":  cty.UnknownVal(cty.String),
  1819  				"ami": cty.StringVal("ami-STATIC"),
  1820  				"list_field": cty.ListVal([]cty.Value{
  1821  					cty.StringVal("aaaa"),
  1822  					cty.StringVal("bbbb"),
  1823  					cty.StringVal("cccc"),
  1824  					cty.StringVal("dddd"),
  1825  					cty.StringVal("eeee"),
  1826  					cty.StringVal("ffff"),
  1827  				}),
  1828  			}),
  1829  			Schema: &configschema.Block{
  1830  				Attributes: map[string]*configschema.Attribute{
  1831  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1832  					"ami":        {Type: cty.String, Optional: true},
  1833  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1834  				},
  1835  			},
  1836  			RequiredReplace: cty.NewPathSet(),
  1837  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1838    ~ resource "test_instance" "example" {
  1839        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1840        ~ list_field = [
  1841              # (1 unchanged element hidden)
  1842              "bbbb",
  1843            + "cccc",
  1844              "dddd",
  1845              # (2 unchanged elements hidden)
  1846          ]
  1847          # (1 unchanged attribute hidden)
  1848      }`,
  1849  		},
  1850  		"force-new update - insertion": {
  1851  			Action:       plans.DeleteThenCreate,
  1852  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  1853  			Mode:         addrs.ManagedResourceMode,
  1854  			Before: cty.ObjectVal(map[string]cty.Value{
  1855  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1856  				"ami": cty.StringVal("ami-STATIC"),
  1857  				"list_field": cty.ListVal([]cty.Value{
  1858  					cty.StringVal("aaaa"),
  1859  					cty.StringVal("cccc"),
  1860  				}),
  1861  			}),
  1862  			After: cty.ObjectVal(map[string]cty.Value{
  1863  				"id":  cty.UnknownVal(cty.String),
  1864  				"ami": cty.StringVal("ami-STATIC"),
  1865  				"list_field": cty.ListVal([]cty.Value{
  1866  					cty.StringVal("aaaa"),
  1867  					cty.StringVal("bbbb"),
  1868  					cty.StringVal("cccc"),
  1869  				}),
  1870  			}),
  1871  			Schema: &configschema.Block{
  1872  				Attributes: map[string]*configschema.Attribute{
  1873  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1874  					"ami":        {Type: cty.String, Optional: true},
  1875  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1876  				},
  1877  			},
  1878  			RequiredReplace: cty.NewPathSet(cty.Path{
  1879  				cty.GetAttrStep{Name: "list_field"},
  1880  			}),
  1881  			ExpectedOutput: `  # test_instance.example must be replaced
  1882  -/+ resource "test_instance" "example" {
  1883        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1884        ~ list_field = [ # forces replacement
  1885              "aaaa",
  1886            + "bbbb",
  1887              "cccc",
  1888          ]
  1889          # (1 unchanged attribute hidden)
  1890      }`,
  1891  		},
  1892  		"in-place update - deletion": {
  1893  			Action: plans.Update,
  1894  			Mode:   addrs.ManagedResourceMode,
  1895  			Before: cty.ObjectVal(map[string]cty.Value{
  1896  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1897  				"ami": cty.StringVal("ami-STATIC"),
  1898  				"list_field": cty.ListVal([]cty.Value{
  1899  					cty.StringVal("aaaa"),
  1900  					cty.StringVal("bbbb"),
  1901  					cty.StringVal("cccc"),
  1902  					cty.StringVal("dddd"),
  1903  					cty.StringVal("eeee"),
  1904  				}),
  1905  			}),
  1906  			After: cty.ObjectVal(map[string]cty.Value{
  1907  				"id":  cty.UnknownVal(cty.String),
  1908  				"ami": cty.StringVal("ami-STATIC"),
  1909  				"list_field": cty.ListVal([]cty.Value{
  1910  					cty.StringVal("bbbb"),
  1911  					cty.StringVal("dddd"),
  1912  					cty.StringVal("eeee"),
  1913  				}),
  1914  			}),
  1915  			Schema: &configschema.Block{
  1916  				Attributes: map[string]*configschema.Attribute{
  1917  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1918  					"ami":        {Type: cty.String, Optional: true},
  1919  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1920  				},
  1921  			},
  1922  			RequiredReplace: cty.NewPathSet(),
  1923  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1924    ~ resource "test_instance" "example" {
  1925        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1926        ~ list_field = [
  1927            - "aaaa",
  1928              "bbbb",
  1929            - "cccc",
  1930              "dddd",
  1931              # (1 unchanged element hidden)
  1932          ]
  1933          # (1 unchanged attribute hidden)
  1934      }`,
  1935  		},
  1936  		"creation - empty list": {
  1937  			Action: plans.Create,
  1938  			Mode:   addrs.ManagedResourceMode,
  1939  			Before: cty.NullVal(cty.EmptyObject),
  1940  			After: cty.ObjectVal(map[string]cty.Value{
  1941  				"id":         cty.UnknownVal(cty.String),
  1942  				"ami":        cty.StringVal("ami-STATIC"),
  1943  				"list_field": cty.ListValEmpty(cty.String),
  1944  			}),
  1945  			Schema: &configschema.Block{
  1946  				Attributes: map[string]*configschema.Attribute{
  1947  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1948  					"ami":        {Type: cty.String, Optional: true},
  1949  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1950  				},
  1951  			},
  1952  			RequiredReplace: cty.NewPathSet(),
  1953  			ExpectedOutput: `  # test_instance.example will be created
  1954    + resource "test_instance" "example" {
  1955        + ami        = "ami-STATIC"
  1956        + id         = (known after apply)
  1957        + list_field = []
  1958      }`,
  1959  		},
  1960  		"in-place update - full to empty": {
  1961  			Action: plans.Update,
  1962  			Mode:   addrs.ManagedResourceMode,
  1963  			Before: cty.ObjectVal(map[string]cty.Value{
  1964  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1965  				"ami": cty.StringVal("ami-STATIC"),
  1966  				"list_field": cty.ListVal([]cty.Value{
  1967  					cty.StringVal("aaaa"),
  1968  					cty.StringVal("bbbb"),
  1969  					cty.StringVal("cccc"),
  1970  				}),
  1971  			}),
  1972  			After: cty.ObjectVal(map[string]cty.Value{
  1973  				"id":         cty.UnknownVal(cty.String),
  1974  				"ami":        cty.StringVal("ami-STATIC"),
  1975  				"list_field": cty.ListValEmpty(cty.String),
  1976  			}),
  1977  			Schema: &configschema.Block{
  1978  				Attributes: map[string]*configschema.Attribute{
  1979  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1980  					"ami":        {Type: cty.String, Optional: true},
  1981  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1982  				},
  1983  			},
  1984  			RequiredReplace: cty.NewPathSet(),
  1985  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1986    ~ resource "test_instance" "example" {
  1987        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1988        ~ list_field = [
  1989            - "aaaa",
  1990            - "bbbb",
  1991            - "cccc",
  1992          ]
  1993          # (1 unchanged attribute hidden)
  1994      }`,
  1995  		},
  1996  		"in-place update - null to empty": {
  1997  			Action: plans.Update,
  1998  			Mode:   addrs.ManagedResourceMode,
  1999  			Before: cty.ObjectVal(map[string]cty.Value{
  2000  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  2001  				"ami":        cty.StringVal("ami-STATIC"),
  2002  				"list_field": cty.NullVal(cty.List(cty.String)),
  2003  			}),
  2004  			After: cty.ObjectVal(map[string]cty.Value{
  2005  				"id":         cty.UnknownVal(cty.String),
  2006  				"ami":        cty.StringVal("ami-STATIC"),
  2007  				"list_field": cty.ListValEmpty(cty.String),
  2008  			}),
  2009  			Schema: &configschema.Block{
  2010  				Attributes: map[string]*configschema.Attribute{
  2011  					"id":         {Type: cty.String, Optional: true, Computed: true},
  2012  					"ami":        {Type: cty.String, Optional: true},
  2013  					"list_field": {Type: cty.List(cty.String), Optional: true},
  2014  				},
  2015  			},
  2016  			RequiredReplace: cty.NewPathSet(),
  2017  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2018    ~ resource "test_instance" "example" {
  2019        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  2020        + list_field = []
  2021          # (1 unchanged attribute hidden)
  2022      }`,
  2023  		},
  2024  		"update to unknown element": {
  2025  			Action: plans.Update,
  2026  			Mode:   addrs.ManagedResourceMode,
  2027  			Before: cty.ObjectVal(map[string]cty.Value{
  2028  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2029  				"ami": cty.StringVal("ami-STATIC"),
  2030  				"list_field": cty.ListVal([]cty.Value{
  2031  					cty.StringVal("aaaa"),
  2032  					cty.StringVal("bbbb"),
  2033  					cty.StringVal("cccc"),
  2034  				}),
  2035  			}),
  2036  			After: cty.ObjectVal(map[string]cty.Value{
  2037  				"id":  cty.UnknownVal(cty.String),
  2038  				"ami": cty.StringVal("ami-STATIC"),
  2039  				"list_field": cty.ListVal([]cty.Value{
  2040  					cty.StringVal("aaaa"),
  2041  					cty.UnknownVal(cty.String),
  2042  					cty.StringVal("cccc"),
  2043  				}),
  2044  			}),
  2045  			Schema: &configschema.Block{
  2046  				Attributes: map[string]*configschema.Attribute{
  2047  					"id":         {Type: cty.String, Optional: true, Computed: true},
  2048  					"ami":        {Type: cty.String, Optional: true},
  2049  					"list_field": {Type: cty.List(cty.String), Optional: true},
  2050  				},
  2051  			},
  2052  			RequiredReplace: cty.NewPathSet(),
  2053  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2054    ~ resource "test_instance" "example" {
  2055        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  2056        ~ list_field = [
  2057              "aaaa",
  2058            - "bbbb",
  2059            + (known after apply),
  2060              "cccc",
  2061          ]
  2062          # (1 unchanged attribute hidden)
  2063      }`,
  2064  		},
  2065  		"update - two new unknown elements": {
  2066  			Action: plans.Update,
  2067  			Mode:   addrs.ManagedResourceMode,
  2068  			Before: cty.ObjectVal(map[string]cty.Value{
  2069  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2070  				"ami": cty.StringVal("ami-STATIC"),
  2071  				"list_field": cty.ListVal([]cty.Value{
  2072  					cty.StringVal("aaaa"),
  2073  					cty.StringVal("bbbb"),
  2074  					cty.StringVal("cccc"),
  2075  					cty.StringVal("dddd"),
  2076  					cty.StringVal("eeee"),
  2077  				}),
  2078  			}),
  2079  			After: cty.ObjectVal(map[string]cty.Value{
  2080  				"id":  cty.UnknownVal(cty.String),
  2081  				"ami": cty.StringVal("ami-STATIC"),
  2082  				"list_field": cty.ListVal([]cty.Value{
  2083  					cty.StringVal("aaaa"),
  2084  					cty.UnknownVal(cty.String),
  2085  					cty.UnknownVal(cty.String),
  2086  					cty.StringVal("cccc"),
  2087  					cty.StringVal("dddd"),
  2088  					cty.StringVal("eeee"),
  2089  				}),
  2090  			}),
  2091  			Schema: &configschema.Block{
  2092  				Attributes: map[string]*configschema.Attribute{
  2093  					"id":         {Type: cty.String, Optional: true, Computed: true},
  2094  					"ami":        {Type: cty.String, Optional: true},
  2095  					"list_field": {Type: cty.List(cty.String), Optional: true},
  2096  				},
  2097  			},
  2098  			RequiredReplace: cty.NewPathSet(),
  2099  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2100    ~ resource "test_instance" "example" {
  2101        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  2102        ~ list_field = [
  2103              "aaaa",
  2104            - "bbbb",
  2105            + (known after apply),
  2106            + (known after apply),
  2107              "cccc",
  2108              # (2 unchanged elements hidden)
  2109          ]
  2110          # (1 unchanged attribute hidden)
  2111      }`,
  2112  		},
  2113  	}
  2114  	runTestCases(t, testCases)
  2115  }
  2116  
  2117  func TestResourceChange_primitiveTuple(t *testing.T) {
  2118  	testCases := map[string]testCase{
  2119  		"in-place update": {
  2120  			Action: plans.Update,
  2121  			Mode:   addrs.ManagedResourceMode,
  2122  			Before: cty.ObjectVal(map[string]cty.Value{
  2123  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  2124  				"tuple_field": cty.TupleVal([]cty.Value{
  2125  					cty.StringVal("aaaa"),
  2126  					cty.StringVal("bbbb"),
  2127  					cty.StringVal("dddd"),
  2128  					cty.StringVal("eeee"),
  2129  					cty.StringVal("ffff"),
  2130  				}),
  2131  			}),
  2132  			After: cty.ObjectVal(map[string]cty.Value{
  2133  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  2134  				"tuple_field": cty.TupleVal([]cty.Value{
  2135  					cty.StringVal("aaaa"),
  2136  					cty.StringVal("bbbb"),
  2137  					cty.StringVal("cccc"),
  2138  					cty.StringVal("eeee"),
  2139  					cty.StringVal("ffff"),
  2140  				}),
  2141  			}),
  2142  			Schema: &configschema.Block{
  2143  				Attributes: map[string]*configschema.Attribute{
  2144  					"id":          {Type: cty.String, Required: true},
  2145  					"tuple_field": {Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.String, cty.String, cty.String}), Optional: true},
  2146  				},
  2147  			},
  2148  			RequiredReplace: cty.NewPathSet(),
  2149  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2150    ~ resource "test_instance" "example" {
  2151          id          = "i-02ae66f368e8518a9"
  2152        ~ tuple_field = [
  2153              # (1 unchanged element hidden)
  2154              "bbbb",
  2155            ~ "dddd" -> "cccc",
  2156              "eeee",
  2157              # (1 unchanged element hidden)
  2158          ]
  2159      }`,
  2160  		},
  2161  	}
  2162  	runTestCases(t, testCases)
  2163  }
  2164  
  2165  func TestResourceChange_primitiveSet(t *testing.T) {
  2166  	testCases := map[string]testCase{
  2167  		"in-place update - creation": {
  2168  			Action: plans.Update,
  2169  			Mode:   addrs.ManagedResourceMode,
  2170  			Before: cty.ObjectVal(map[string]cty.Value{
  2171  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
  2172  				"ami":       cty.StringVal("ami-STATIC"),
  2173  				"set_field": cty.NullVal(cty.Set(cty.String)),
  2174  			}),
  2175  			After: cty.ObjectVal(map[string]cty.Value{
  2176  				"id":  cty.UnknownVal(cty.String),
  2177  				"ami": cty.StringVal("ami-STATIC"),
  2178  				"set_field": cty.SetVal([]cty.Value{
  2179  					cty.StringVal("new-element"),
  2180  				}),
  2181  			}),
  2182  			Schema: &configschema.Block{
  2183  				Attributes: map[string]*configschema.Attribute{
  2184  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2185  					"ami":       {Type: cty.String, Optional: true},
  2186  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  2187  				},
  2188  			},
  2189  			RequiredReplace: cty.NewPathSet(),
  2190  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2191    ~ resource "test_instance" "example" {
  2192        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2193        + set_field = [
  2194            + "new-element",
  2195          ]
  2196          # (1 unchanged attribute hidden)
  2197      }`,
  2198  		},
  2199  		"in-place update - first insertion": {
  2200  			Action: plans.Update,
  2201  			Mode:   addrs.ManagedResourceMode,
  2202  			Before: cty.ObjectVal(map[string]cty.Value{
  2203  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
  2204  				"ami":       cty.StringVal("ami-STATIC"),
  2205  				"set_field": cty.SetValEmpty(cty.String),
  2206  			}),
  2207  			After: cty.ObjectVal(map[string]cty.Value{
  2208  				"id":  cty.UnknownVal(cty.String),
  2209  				"ami": cty.StringVal("ami-STATIC"),
  2210  				"set_field": cty.SetVal([]cty.Value{
  2211  					cty.StringVal("new-element"),
  2212  				}),
  2213  			}),
  2214  			Schema: &configschema.Block{
  2215  				Attributes: map[string]*configschema.Attribute{
  2216  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2217  					"ami":       {Type: cty.String, Optional: true},
  2218  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  2219  				},
  2220  			},
  2221  			RequiredReplace: cty.NewPathSet(),
  2222  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2223    ~ resource "test_instance" "example" {
  2224        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2225        ~ set_field = [
  2226            + "new-element",
  2227          ]
  2228          # (1 unchanged attribute hidden)
  2229      }`,
  2230  		},
  2231  		"in-place update - insertion": {
  2232  			Action: plans.Update,
  2233  			Mode:   addrs.ManagedResourceMode,
  2234  			Before: cty.ObjectVal(map[string]cty.Value{
  2235  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2236  				"ami": cty.StringVal("ami-STATIC"),
  2237  				"set_field": cty.SetVal([]cty.Value{
  2238  					cty.StringVal("aaaa"),
  2239  					cty.StringVal("cccc"),
  2240  				}),
  2241  			}),
  2242  			After: cty.ObjectVal(map[string]cty.Value{
  2243  				"id":  cty.UnknownVal(cty.String),
  2244  				"ami": cty.StringVal("ami-STATIC"),
  2245  				"set_field": cty.SetVal([]cty.Value{
  2246  					cty.StringVal("aaaa"),
  2247  					cty.StringVal("bbbb"),
  2248  					cty.StringVal("cccc"),
  2249  				}),
  2250  			}),
  2251  			Schema: &configschema.Block{
  2252  				Attributes: map[string]*configschema.Attribute{
  2253  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2254  					"ami":       {Type: cty.String, Optional: true},
  2255  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  2256  				},
  2257  			},
  2258  			RequiredReplace: cty.NewPathSet(),
  2259  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2260    ~ resource "test_instance" "example" {
  2261        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2262        ~ set_field = [
  2263            + "bbbb",
  2264              # (2 unchanged elements hidden)
  2265          ]
  2266          # (1 unchanged attribute hidden)
  2267      }`,
  2268  		},
  2269  		"force-new update - insertion": {
  2270  			Action:       plans.DeleteThenCreate,
  2271  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  2272  			Mode:         addrs.ManagedResourceMode,
  2273  			Before: cty.ObjectVal(map[string]cty.Value{
  2274  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2275  				"ami": cty.StringVal("ami-STATIC"),
  2276  				"set_field": cty.SetVal([]cty.Value{
  2277  					cty.StringVal("aaaa"),
  2278  					cty.StringVal("cccc"),
  2279  				}),
  2280  			}),
  2281  			After: cty.ObjectVal(map[string]cty.Value{
  2282  				"id":  cty.UnknownVal(cty.String),
  2283  				"ami": cty.StringVal("ami-STATIC"),
  2284  				"set_field": cty.SetVal([]cty.Value{
  2285  					cty.StringVal("aaaa"),
  2286  					cty.StringVal("bbbb"),
  2287  					cty.StringVal("cccc"),
  2288  				}),
  2289  			}),
  2290  			Schema: &configschema.Block{
  2291  				Attributes: map[string]*configschema.Attribute{
  2292  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2293  					"ami":       {Type: cty.String, Optional: true},
  2294  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  2295  				},
  2296  			},
  2297  			RequiredReplace: cty.NewPathSet(cty.Path{
  2298  				cty.GetAttrStep{Name: "set_field"},
  2299  			}),
  2300  			ExpectedOutput: `  # test_instance.example must be replaced
  2301  -/+ resource "test_instance" "example" {
  2302        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2303        ~ set_field = [ # forces replacement
  2304            + "bbbb",
  2305              # (2 unchanged elements hidden)
  2306          ]
  2307          # (1 unchanged attribute hidden)
  2308      }`,
  2309  		},
  2310  		"in-place update - deletion": {
  2311  			Action: plans.Update,
  2312  			Mode:   addrs.ManagedResourceMode,
  2313  			Before: cty.ObjectVal(map[string]cty.Value{
  2314  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2315  				"ami": cty.StringVal("ami-STATIC"),
  2316  				"set_field": cty.SetVal([]cty.Value{
  2317  					cty.StringVal("aaaa"),
  2318  					cty.StringVal("bbbb"),
  2319  					cty.StringVal("cccc"),
  2320  				}),
  2321  			}),
  2322  			After: cty.ObjectVal(map[string]cty.Value{
  2323  				"id":  cty.UnknownVal(cty.String),
  2324  				"ami": cty.StringVal("ami-STATIC"),
  2325  				"set_field": cty.SetVal([]cty.Value{
  2326  					cty.StringVal("bbbb"),
  2327  				}),
  2328  			}),
  2329  			Schema: &configschema.Block{
  2330  				Attributes: map[string]*configschema.Attribute{
  2331  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2332  					"ami":       {Type: cty.String, Optional: true},
  2333  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  2334  				},
  2335  			},
  2336  			RequiredReplace: cty.NewPathSet(),
  2337  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2338    ~ resource "test_instance" "example" {
  2339        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2340        ~ set_field = [
  2341            - "aaaa",
  2342            - "cccc",
  2343              # (1 unchanged element hidden)
  2344          ]
  2345          # (1 unchanged attribute hidden)
  2346      }`,
  2347  		},
  2348  		"creation - empty set": {
  2349  			Action: plans.Create,
  2350  			Mode:   addrs.ManagedResourceMode,
  2351  			Before: cty.NullVal(cty.EmptyObject),
  2352  			After: cty.ObjectVal(map[string]cty.Value{
  2353  				"id":        cty.UnknownVal(cty.String),
  2354  				"ami":       cty.StringVal("ami-STATIC"),
  2355  				"set_field": cty.SetValEmpty(cty.String),
  2356  			}),
  2357  			Schema: &configschema.Block{
  2358  				Attributes: map[string]*configschema.Attribute{
  2359  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2360  					"ami":       {Type: cty.String, Optional: true},
  2361  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  2362  				},
  2363  			},
  2364  			RequiredReplace: cty.NewPathSet(),
  2365  			ExpectedOutput: `  # test_instance.example will be created
  2366    + resource "test_instance" "example" {
  2367        + ami       = "ami-STATIC"
  2368        + id        = (known after apply)
  2369        + set_field = []
  2370      }`,
  2371  		},
  2372  		"in-place update - full to empty set": {
  2373  			Action: plans.Update,
  2374  			Mode:   addrs.ManagedResourceMode,
  2375  			Before: cty.ObjectVal(map[string]cty.Value{
  2376  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2377  				"ami": cty.StringVal("ami-STATIC"),
  2378  				"set_field": cty.SetVal([]cty.Value{
  2379  					cty.StringVal("aaaa"),
  2380  					cty.StringVal("bbbb"),
  2381  				}),
  2382  			}),
  2383  			After: cty.ObjectVal(map[string]cty.Value{
  2384  				"id":        cty.UnknownVal(cty.String),
  2385  				"ami":       cty.StringVal("ami-STATIC"),
  2386  				"set_field": cty.SetValEmpty(cty.String),
  2387  			}),
  2388  			Schema: &configschema.Block{
  2389  				Attributes: map[string]*configschema.Attribute{
  2390  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2391  					"ami":       {Type: cty.String, Optional: true},
  2392  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  2393  				},
  2394  			},
  2395  			RequiredReplace: cty.NewPathSet(),
  2396  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2397    ~ resource "test_instance" "example" {
  2398        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2399        ~ set_field = [
  2400            - "aaaa",
  2401            - "bbbb",
  2402          ]
  2403          # (1 unchanged attribute hidden)
  2404      }`,
  2405  		},
  2406  		"in-place update - null to empty set": {
  2407  			Action: plans.Update,
  2408  			Mode:   addrs.ManagedResourceMode,
  2409  			Before: cty.ObjectVal(map[string]cty.Value{
  2410  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
  2411  				"ami":       cty.StringVal("ami-STATIC"),
  2412  				"set_field": cty.NullVal(cty.Set(cty.String)),
  2413  			}),
  2414  			After: cty.ObjectVal(map[string]cty.Value{
  2415  				"id":        cty.UnknownVal(cty.String),
  2416  				"ami":       cty.StringVal("ami-STATIC"),
  2417  				"set_field": cty.SetValEmpty(cty.String),
  2418  			}),
  2419  			Schema: &configschema.Block{
  2420  				Attributes: map[string]*configschema.Attribute{
  2421  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2422  					"ami":       {Type: cty.String, Optional: true},
  2423  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  2424  				},
  2425  			},
  2426  			RequiredReplace: cty.NewPathSet(),
  2427  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2428    ~ resource "test_instance" "example" {
  2429        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2430        + set_field = []
  2431          # (1 unchanged attribute hidden)
  2432      }`,
  2433  		},
  2434  		"in-place update to unknown": {
  2435  			Action: plans.Update,
  2436  			Mode:   addrs.ManagedResourceMode,
  2437  			Before: cty.ObjectVal(map[string]cty.Value{
  2438  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2439  				"ami": cty.StringVal("ami-STATIC"),
  2440  				"set_field": cty.SetVal([]cty.Value{
  2441  					cty.StringVal("aaaa"),
  2442  					cty.StringVal("bbbb"),
  2443  				}),
  2444  			}),
  2445  			After: cty.ObjectVal(map[string]cty.Value{
  2446  				"id":        cty.UnknownVal(cty.String),
  2447  				"ami":       cty.StringVal("ami-STATIC"),
  2448  				"set_field": cty.UnknownVal(cty.Set(cty.String)),
  2449  			}),
  2450  			Schema: &configschema.Block{
  2451  				Attributes: map[string]*configschema.Attribute{
  2452  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2453  					"ami":       {Type: cty.String, Optional: true},
  2454  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  2455  				},
  2456  			},
  2457  			RequiredReplace: cty.NewPathSet(),
  2458  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2459    ~ resource "test_instance" "example" {
  2460        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2461        ~ set_field = [
  2462            - "aaaa",
  2463            - "bbbb",
  2464          ] -> (known after apply)
  2465          # (1 unchanged attribute hidden)
  2466      }`,
  2467  		},
  2468  		"in-place update to unknown element": {
  2469  			Action: plans.Update,
  2470  			Mode:   addrs.ManagedResourceMode,
  2471  			Before: cty.ObjectVal(map[string]cty.Value{
  2472  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2473  				"ami": cty.StringVal("ami-STATIC"),
  2474  				"set_field": cty.SetVal([]cty.Value{
  2475  					cty.StringVal("aaaa"),
  2476  					cty.StringVal("bbbb"),
  2477  				}),
  2478  			}),
  2479  			After: cty.ObjectVal(map[string]cty.Value{
  2480  				"id":  cty.UnknownVal(cty.String),
  2481  				"ami": cty.StringVal("ami-STATIC"),
  2482  				"set_field": cty.SetVal([]cty.Value{
  2483  					cty.StringVal("aaaa"),
  2484  					cty.UnknownVal(cty.String),
  2485  				}),
  2486  			}),
  2487  			Schema: &configschema.Block{
  2488  				Attributes: map[string]*configschema.Attribute{
  2489  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2490  					"ami":       {Type: cty.String, Optional: true},
  2491  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  2492  				},
  2493  			},
  2494  			RequiredReplace: cty.NewPathSet(),
  2495  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2496    ~ resource "test_instance" "example" {
  2497        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2498        ~ set_field = [
  2499            - "bbbb",
  2500            + (known after apply),
  2501              # (1 unchanged element hidden)
  2502          ]
  2503          # (1 unchanged attribute hidden)
  2504      }`,
  2505  		},
  2506  	}
  2507  	runTestCases(t, testCases)
  2508  }
  2509  
  2510  func TestResourceChange_map(t *testing.T) {
  2511  	testCases := map[string]testCase{
  2512  		"in-place update - creation": {
  2513  			Action: plans.Update,
  2514  			Mode:   addrs.ManagedResourceMode,
  2515  			Before: cty.ObjectVal(map[string]cty.Value{
  2516  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
  2517  				"ami":       cty.StringVal("ami-STATIC"),
  2518  				"map_field": cty.NullVal(cty.Map(cty.String)),
  2519  			}),
  2520  			After: cty.ObjectVal(map[string]cty.Value{
  2521  				"id":  cty.UnknownVal(cty.String),
  2522  				"ami": cty.StringVal("ami-STATIC"),
  2523  				"map_field": cty.MapVal(map[string]cty.Value{
  2524  					"new-key": cty.StringVal("new-element"),
  2525  					"be:ep":   cty.StringVal("boop"),
  2526  				}),
  2527  			}),
  2528  			Schema: &configschema.Block{
  2529  				Attributes: map[string]*configschema.Attribute{
  2530  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2531  					"ami":       {Type: cty.String, Optional: true},
  2532  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  2533  				},
  2534  			},
  2535  			RequiredReplace: cty.NewPathSet(),
  2536  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2537    ~ resource "test_instance" "example" {
  2538        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2539        + map_field = {
  2540            + "be:ep"   = "boop"
  2541            + "new-key" = "new-element"
  2542          }
  2543          # (1 unchanged attribute hidden)
  2544      }`,
  2545  		},
  2546  		"in-place update - first insertion": {
  2547  			Action: plans.Update,
  2548  			Mode:   addrs.ManagedResourceMode,
  2549  			Before: cty.ObjectVal(map[string]cty.Value{
  2550  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
  2551  				"ami":       cty.StringVal("ami-STATIC"),
  2552  				"map_field": cty.MapValEmpty(cty.String),
  2553  			}),
  2554  			After: cty.ObjectVal(map[string]cty.Value{
  2555  				"id":  cty.UnknownVal(cty.String),
  2556  				"ami": cty.StringVal("ami-STATIC"),
  2557  				"map_field": cty.MapVal(map[string]cty.Value{
  2558  					"new-key": cty.StringVal("new-element"),
  2559  					"be:ep":   cty.StringVal("boop"),
  2560  				}),
  2561  			}),
  2562  			Schema: &configschema.Block{
  2563  				Attributes: map[string]*configschema.Attribute{
  2564  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2565  					"ami":       {Type: cty.String, Optional: true},
  2566  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  2567  				},
  2568  			},
  2569  			RequiredReplace: cty.NewPathSet(),
  2570  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2571    ~ resource "test_instance" "example" {
  2572        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2573        ~ map_field = {
  2574            + "be:ep"   = "boop"
  2575            + "new-key" = "new-element"
  2576          }
  2577          # (1 unchanged attribute hidden)
  2578      }`,
  2579  		},
  2580  		"in-place update - insertion": {
  2581  			Action: plans.Update,
  2582  			Mode:   addrs.ManagedResourceMode,
  2583  			Before: cty.ObjectVal(map[string]cty.Value{
  2584  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2585  				"ami": cty.StringVal("ami-STATIC"),
  2586  				"map_field": cty.MapVal(map[string]cty.Value{
  2587  					"a": cty.StringVal("aaaa"),
  2588  					"c": cty.StringVal("cccc"),
  2589  				}),
  2590  			}),
  2591  			After: cty.ObjectVal(map[string]cty.Value{
  2592  				"id":  cty.UnknownVal(cty.String),
  2593  				"ami": cty.StringVal("ami-STATIC"),
  2594  				"map_field": cty.MapVal(map[string]cty.Value{
  2595  					"a":   cty.StringVal("aaaa"),
  2596  					"b":   cty.StringVal("bbbb"),
  2597  					"b:b": cty.StringVal("bbbb"),
  2598  					"c":   cty.StringVal("cccc"),
  2599  				}),
  2600  			}),
  2601  			Schema: &configschema.Block{
  2602  				Attributes: map[string]*configschema.Attribute{
  2603  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2604  					"ami":       {Type: cty.String, Optional: true},
  2605  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  2606  				},
  2607  			},
  2608  			RequiredReplace: cty.NewPathSet(),
  2609  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2610    ~ resource "test_instance" "example" {
  2611        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2612        ~ map_field = {
  2613            + "b"   = "bbbb"
  2614            + "b:b" = "bbbb"
  2615              # (2 unchanged elements hidden)
  2616          }
  2617          # (1 unchanged attribute hidden)
  2618      }`,
  2619  		},
  2620  		"force-new update - insertion": {
  2621  			Action:       plans.DeleteThenCreate,
  2622  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  2623  			Mode:         addrs.ManagedResourceMode,
  2624  			Before: cty.ObjectVal(map[string]cty.Value{
  2625  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2626  				"ami": cty.StringVal("ami-STATIC"),
  2627  				"map_field": cty.MapVal(map[string]cty.Value{
  2628  					"a": cty.StringVal("aaaa"),
  2629  					"c": cty.StringVal("cccc"),
  2630  				}),
  2631  			}),
  2632  			After: cty.ObjectVal(map[string]cty.Value{
  2633  				"id":  cty.UnknownVal(cty.String),
  2634  				"ami": cty.StringVal("ami-STATIC"),
  2635  				"map_field": cty.MapVal(map[string]cty.Value{
  2636  					"a": cty.StringVal("aaaa"),
  2637  					"b": cty.StringVal("bbbb"),
  2638  					"c": cty.StringVal("cccc"),
  2639  				}),
  2640  			}),
  2641  			Schema: &configschema.Block{
  2642  				Attributes: map[string]*configschema.Attribute{
  2643  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2644  					"ami":       {Type: cty.String, Optional: true},
  2645  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  2646  				},
  2647  			},
  2648  			RequiredReplace: cty.NewPathSet(cty.Path{
  2649  				cty.GetAttrStep{Name: "map_field"},
  2650  			}),
  2651  			ExpectedOutput: `  # test_instance.example must be replaced
  2652  -/+ resource "test_instance" "example" {
  2653        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2654        ~ map_field = { # forces replacement
  2655            + "b" = "bbbb"
  2656              # (2 unchanged elements hidden)
  2657          }
  2658          # (1 unchanged attribute hidden)
  2659      }`,
  2660  		},
  2661  		"in-place update - deletion": {
  2662  			Action: plans.Update,
  2663  			Mode:   addrs.ManagedResourceMode,
  2664  			Before: cty.ObjectVal(map[string]cty.Value{
  2665  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2666  				"ami": cty.StringVal("ami-STATIC"),
  2667  				"map_field": cty.MapVal(map[string]cty.Value{
  2668  					"a": cty.StringVal("aaaa"),
  2669  					"b": cty.StringVal("bbbb"),
  2670  					"c": cty.StringVal("cccc"),
  2671  				}),
  2672  			}),
  2673  			After: cty.ObjectVal(map[string]cty.Value{
  2674  				"id":  cty.UnknownVal(cty.String),
  2675  				"ami": cty.StringVal("ami-STATIC"),
  2676  				"map_field": cty.MapVal(map[string]cty.Value{
  2677  					"b": cty.StringVal("bbbb"),
  2678  				}),
  2679  			}),
  2680  			Schema: &configschema.Block{
  2681  				Attributes: map[string]*configschema.Attribute{
  2682  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2683  					"ami":       {Type: cty.String, Optional: true},
  2684  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  2685  				},
  2686  			},
  2687  			RequiredReplace: cty.NewPathSet(),
  2688  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2689    ~ resource "test_instance" "example" {
  2690        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2691        ~ map_field = {
  2692            - "a" = "aaaa" -> null
  2693            - "c" = "cccc" -> null
  2694              # (1 unchanged element hidden)
  2695          }
  2696          # (1 unchanged attribute hidden)
  2697      }`,
  2698  		},
  2699  		"creation - empty": {
  2700  			Action: plans.Create,
  2701  			Mode:   addrs.ManagedResourceMode,
  2702  			Before: cty.NullVal(cty.EmptyObject),
  2703  			After: cty.ObjectVal(map[string]cty.Value{
  2704  				"id":        cty.UnknownVal(cty.String),
  2705  				"ami":       cty.StringVal("ami-STATIC"),
  2706  				"map_field": cty.MapValEmpty(cty.String),
  2707  			}),
  2708  			Schema: &configschema.Block{
  2709  				Attributes: map[string]*configschema.Attribute{
  2710  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2711  					"ami":       {Type: cty.String, Optional: true},
  2712  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  2713  				},
  2714  			},
  2715  			RequiredReplace: cty.NewPathSet(),
  2716  			ExpectedOutput: `  # test_instance.example will be created
  2717    + resource "test_instance" "example" {
  2718        + ami       = "ami-STATIC"
  2719        + id        = (known after apply)
  2720        + map_field = {}
  2721      }`,
  2722  		},
  2723  		"update to unknown element": {
  2724  			Action: plans.Update,
  2725  			Mode:   addrs.ManagedResourceMode,
  2726  			Before: cty.ObjectVal(map[string]cty.Value{
  2727  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2728  				"ami": cty.StringVal("ami-STATIC"),
  2729  				"map_field": cty.MapVal(map[string]cty.Value{
  2730  					"a": cty.StringVal("aaaa"),
  2731  					"b": cty.StringVal("bbbb"),
  2732  					"c": cty.StringVal("cccc"),
  2733  				}),
  2734  			}),
  2735  			After: cty.ObjectVal(map[string]cty.Value{
  2736  				"id":  cty.UnknownVal(cty.String),
  2737  				"ami": cty.StringVal("ami-STATIC"),
  2738  				"map_field": cty.MapVal(map[string]cty.Value{
  2739  					"a": cty.StringVal("aaaa"),
  2740  					"b": cty.UnknownVal(cty.String),
  2741  					"c": cty.StringVal("cccc"),
  2742  				}),
  2743  			}),
  2744  			Schema: &configschema.Block{
  2745  				Attributes: map[string]*configschema.Attribute{
  2746  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2747  					"ami":       {Type: cty.String, Optional: true},
  2748  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  2749  				},
  2750  			},
  2751  			RequiredReplace: cty.NewPathSet(),
  2752  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2753    ~ resource "test_instance" "example" {
  2754        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2755        ~ map_field = {
  2756            ~ "b" = "bbbb" -> (known after apply)
  2757              # (2 unchanged elements hidden)
  2758          }
  2759          # (1 unchanged attribute hidden)
  2760      }`,
  2761  		},
  2762  	}
  2763  	runTestCases(t, testCases)
  2764  }
  2765  
  2766  func TestResourceChange_nestedList(t *testing.T) {
  2767  	testCases := map[string]testCase{
  2768  		"in-place update - equal": {
  2769  			Action: plans.Update,
  2770  			Mode:   addrs.ManagedResourceMode,
  2771  			Before: cty.ObjectVal(map[string]cty.Value{
  2772  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2773  				"ami": cty.StringVal("ami-BEFORE"),
  2774  				"root_block_device": cty.ListVal([]cty.Value{
  2775  					cty.ObjectVal(map[string]cty.Value{
  2776  						"volume_type": cty.StringVal("gp2"),
  2777  					}),
  2778  				}),
  2779  				"disks": cty.ListVal([]cty.Value{
  2780  					cty.ObjectVal(map[string]cty.Value{
  2781  						"mount_point": cty.StringVal("/var/diska"),
  2782  						"size":        cty.StringVal("50GB"),
  2783  					}),
  2784  				}),
  2785  			}),
  2786  			After: cty.ObjectVal(map[string]cty.Value{
  2787  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2788  				"ami": cty.StringVal("ami-AFTER"),
  2789  				"root_block_device": cty.ListVal([]cty.Value{
  2790  					cty.ObjectVal(map[string]cty.Value{
  2791  						"volume_type": cty.StringVal("gp2"),
  2792  					}),
  2793  				}),
  2794  				"disks": cty.ListVal([]cty.Value{
  2795  					cty.ObjectVal(map[string]cty.Value{
  2796  						"mount_point": cty.StringVal("/var/diska"),
  2797  						"size":        cty.StringVal("50GB"),
  2798  					}),
  2799  				}),
  2800  			}),
  2801  			RequiredReplace: cty.NewPathSet(),
  2802  			Schema:          testSchema(configschema.NestingList),
  2803  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2804    ~ resource "test_instance" "example" {
  2805        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2806          id    = "i-02ae66f368e8518a9"
  2807          # (1 unchanged attribute hidden)
  2808  
  2809          # (1 unchanged block hidden)
  2810      }`,
  2811  		},
  2812  		"in-place update - creation": {
  2813  			Action: plans.Update,
  2814  			Mode:   addrs.ManagedResourceMode,
  2815  			Before: cty.ObjectVal(map[string]cty.Value{
  2816  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2817  				"ami": cty.StringVal("ami-BEFORE"),
  2818  				"root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  2819  					"volume_type": cty.String,
  2820  				})),
  2821  				"disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  2822  					"mount_point": cty.String,
  2823  					"size":        cty.String,
  2824  				})),
  2825  			}),
  2826  			After: cty.ObjectVal(map[string]cty.Value{
  2827  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2828  				"ami": cty.StringVal("ami-AFTER"),
  2829  				"disks": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
  2830  					"mount_point": cty.StringVal("/var/diska"),
  2831  					"size":        cty.StringVal("50GB"),
  2832  				})}),
  2833  				"root_block_device": cty.ListVal([]cty.Value{
  2834  					cty.ObjectVal(map[string]cty.Value{
  2835  						"volume_type": cty.NullVal(cty.String),
  2836  					}),
  2837  				}),
  2838  			}),
  2839  			RequiredReplace: cty.NewPathSet(),
  2840  			Schema:          testSchema(configschema.NestingList),
  2841  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2842    ~ resource "test_instance" "example" {
  2843        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2844        ~ disks = [
  2845            + {
  2846                + mount_point = "/var/diska"
  2847                + size        = "50GB"
  2848              },
  2849          ]
  2850          id    = "i-02ae66f368e8518a9"
  2851  
  2852        + root_block_device {}
  2853      }`,
  2854  		},
  2855  		"in-place update - first insertion": {
  2856  			Action: plans.Update,
  2857  			Mode:   addrs.ManagedResourceMode,
  2858  			Before: cty.ObjectVal(map[string]cty.Value{
  2859  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2860  				"ami": cty.StringVal("ami-BEFORE"),
  2861  				"root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  2862  					"volume_type": cty.String,
  2863  				})),
  2864  				"disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  2865  					"mount_point": cty.String,
  2866  					"size":        cty.String,
  2867  				})),
  2868  			}),
  2869  			After: cty.ObjectVal(map[string]cty.Value{
  2870  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2871  				"ami": cty.StringVal("ami-AFTER"),
  2872  				"disks": cty.ListVal([]cty.Value{
  2873  					cty.ObjectVal(map[string]cty.Value{
  2874  						"mount_point": cty.StringVal("/var/diska"),
  2875  						"size":        cty.NullVal(cty.String),
  2876  					}),
  2877  				}),
  2878  				"root_block_device": cty.ListVal([]cty.Value{
  2879  					cty.ObjectVal(map[string]cty.Value{
  2880  						"volume_type": cty.StringVal("gp2"),
  2881  					}),
  2882  				}),
  2883  			}),
  2884  			RequiredReplace: cty.NewPathSet(),
  2885  			Schema:          testSchema(configschema.NestingList),
  2886  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2887    ~ resource "test_instance" "example" {
  2888        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2889        ~ disks = [
  2890            + {
  2891                + mount_point = "/var/diska"
  2892              },
  2893          ]
  2894          id    = "i-02ae66f368e8518a9"
  2895  
  2896        + root_block_device {
  2897            + volume_type = "gp2"
  2898          }
  2899      }`,
  2900  		},
  2901  		"in-place update - insertion": {
  2902  			Action: plans.Update,
  2903  			Mode:   addrs.ManagedResourceMode,
  2904  			Before: cty.ObjectVal(map[string]cty.Value{
  2905  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2906  				"ami": cty.StringVal("ami-BEFORE"),
  2907  				"disks": cty.ListVal([]cty.Value{
  2908  					cty.ObjectVal(map[string]cty.Value{
  2909  						"mount_point": cty.StringVal("/var/diska"),
  2910  						"size":        cty.NullVal(cty.String),
  2911  					}),
  2912  					cty.ObjectVal(map[string]cty.Value{
  2913  						"mount_point": cty.StringVal("/var/diskb"),
  2914  						"size":        cty.StringVal("50GB"),
  2915  					}),
  2916  				}),
  2917  				"root_block_device": cty.ListVal([]cty.Value{
  2918  					cty.ObjectVal(map[string]cty.Value{
  2919  						"volume_type": cty.StringVal("gp2"),
  2920  						"new_field":   cty.NullVal(cty.String),
  2921  					}),
  2922  				}),
  2923  			}),
  2924  			After: cty.ObjectVal(map[string]cty.Value{
  2925  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2926  				"ami": cty.StringVal("ami-AFTER"),
  2927  				"disks": cty.ListVal([]cty.Value{
  2928  					cty.ObjectVal(map[string]cty.Value{
  2929  						"mount_point": cty.StringVal("/var/diska"),
  2930  						"size":        cty.StringVal("50GB"),
  2931  					}),
  2932  					cty.ObjectVal(map[string]cty.Value{
  2933  						"mount_point": cty.StringVal("/var/diskb"),
  2934  						"size":        cty.StringVal("50GB"),
  2935  					}),
  2936  				}),
  2937  				"root_block_device": cty.ListVal([]cty.Value{
  2938  					cty.ObjectVal(map[string]cty.Value{
  2939  						"volume_type": cty.StringVal("gp2"),
  2940  						"new_field":   cty.StringVal("new_value"),
  2941  					}),
  2942  				}),
  2943  			}),
  2944  			RequiredReplace: cty.NewPathSet(),
  2945  			Schema:          testSchemaPlus(configschema.NestingList),
  2946  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2947    ~ resource "test_instance" "example" {
  2948        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2949        ~ disks = [
  2950            ~ {
  2951                + size        = "50GB"
  2952                  # (1 unchanged attribute hidden)
  2953              },
  2954              # (1 unchanged element hidden)
  2955          ]
  2956          id    = "i-02ae66f368e8518a9"
  2957  
  2958        ~ root_block_device {
  2959            + new_field   = "new_value"
  2960              # (1 unchanged attribute hidden)
  2961          }
  2962      }`,
  2963  		},
  2964  		"force-new update (inside blocks)": {
  2965  			Action:       plans.DeleteThenCreate,
  2966  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  2967  			Mode:         addrs.ManagedResourceMode,
  2968  			Before: cty.ObjectVal(map[string]cty.Value{
  2969  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2970  				"ami": cty.StringVal("ami-BEFORE"),
  2971  				"disks": cty.ListVal([]cty.Value{
  2972  					cty.ObjectVal(map[string]cty.Value{
  2973  						"mount_point": cty.StringVal("/var/diska"),
  2974  						"size":        cty.StringVal("50GB"),
  2975  					}),
  2976  				}),
  2977  				"root_block_device": cty.ListVal([]cty.Value{
  2978  					cty.ObjectVal(map[string]cty.Value{
  2979  						"volume_type": cty.StringVal("gp2"),
  2980  					}),
  2981  				}),
  2982  			}),
  2983  			After: cty.ObjectVal(map[string]cty.Value{
  2984  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2985  				"ami": cty.StringVal("ami-AFTER"),
  2986  				"disks": cty.ListVal([]cty.Value{
  2987  					cty.ObjectVal(map[string]cty.Value{
  2988  						"mount_point": cty.StringVal("/var/diskb"),
  2989  						"size":        cty.StringVal("50GB"),
  2990  					}),
  2991  				}),
  2992  				"root_block_device": cty.ListVal([]cty.Value{
  2993  					cty.ObjectVal(map[string]cty.Value{
  2994  						"volume_type": cty.StringVal("different"),
  2995  					}),
  2996  				}),
  2997  			}),
  2998  			RequiredReplace: cty.NewPathSet(
  2999  				cty.Path{
  3000  					cty.GetAttrStep{Name: "root_block_device"},
  3001  					cty.IndexStep{Key: cty.NumberIntVal(0)},
  3002  					cty.GetAttrStep{Name: "volume_type"},
  3003  				},
  3004  				cty.Path{
  3005  					cty.GetAttrStep{Name: "disks"},
  3006  					cty.IndexStep{Key: cty.NumberIntVal(0)},
  3007  					cty.GetAttrStep{Name: "mount_point"},
  3008  				},
  3009  			),
  3010  			Schema: testSchema(configschema.NestingList),
  3011  			ExpectedOutput: `  # test_instance.example must be replaced
  3012  -/+ resource "test_instance" "example" {
  3013        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3014        ~ disks = [
  3015            ~ {
  3016                ~ mount_point = "/var/diska" -> "/var/diskb" # forces replacement
  3017                  # (1 unchanged attribute hidden)
  3018              },
  3019          ]
  3020          id    = "i-02ae66f368e8518a9"
  3021  
  3022        ~ root_block_device {
  3023            ~ volume_type = "gp2" -> "different" # forces replacement
  3024          }
  3025      }`,
  3026  		},
  3027  		"force-new update (whole block)": {
  3028  			Action:       plans.DeleteThenCreate,
  3029  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  3030  			Mode:         addrs.ManagedResourceMode,
  3031  			Before: cty.ObjectVal(map[string]cty.Value{
  3032  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3033  				"ami": cty.StringVal("ami-BEFORE"),
  3034  				"disks": cty.ListVal([]cty.Value{
  3035  					cty.ObjectVal(map[string]cty.Value{
  3036  						"mount_point": cty.StringVal("/var/diska"),
  3037  						"size":        cty.StringVal("50GB"),
  3038  					}),
  3039  				}),
  3040  				"root_block_device": cty.ListVal([]cty.Value{
  3041  					cty.ObjectVal(map[string]cty.Value{
  3042  						"volume_type": cty.StringVal("gp2"),
  3043  					}),
  3044  				}),
  3045  			}),
  3046  			After: cty.ObjectVal(map[string]cty.Value{
  3047  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3048  				"ami": cty.StringVal("ami-AFTER"),
  3049  				"disks": cty.ListVal([]cty.Value{
  3050  					cty.ObjectVal(map[string]cty.Value{
  3051  						"mount_point": cty.StringVal("/var/diskb"),
  3052  						"size":        cty.StringVal("50GB"),
  3053  					}),
  3054  				}),
  3055  				"root_block_device": cty.ListVal([]cty.Value{
  3056  					cty.ObjectVal(map[string]cty.Value{
  3057  						"volume_type": cty.StringVal("different"),
  3058  					}),
  3059  				}),
  3060  			}),
  3061  			RequiredReplace: cty.NewPathSet(
  3062  				cty.Path{cty.GetAttrStep{Name: "root_block_device"}},
  3063  				cty.Path{cty.GetAttrStep{Name: "disks"}},
  3064  			),
  3065  			Schema: testSchema(configschema.NestingList),
  3066  			ExpectedOutput: `  # test_instance.example must be replaced
  3067  -/+ resource "test_instance" "example" {
  3068        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3069        ~ disks = [ # forces replacement
  3070            ~ {
  3071                ~ mount_point = "/var/diska" -> "/var/diskb"
  3072                  # (1 unchanged attribute hidden)
  3073              },
  3074          ]
  3075          id    = "i-02ae66f368e8518a9"
  3076  
  3077        ~ root_block_device { # forces replacement
  3078            ~ volume_type = "gp2" -> "different"
  3079          }
  3080      }`,
  3081  		},
  3082  		"in-place update - deletion": {
  3083  			Action: plans.Update,
  3084  			Mode:   addrs.ManagedResourceMode,
  3085  			Before: cty.ObjectVal(map[string]cty.Value{
  3086  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3087  				"ami": cty.StringVal("ami-BEFORE"),
  3088  				"disks": cty.ListVal([]cty.Value{
  3089  					cty.ObjectVal(map[string]cty.Value{
  3090  						"mount_point": cty.StringVal("/var/diska"),
  3091  						"size":        cty.StringVal("50GB"),
  3092  					}),
  3093  				}),
  3094  				"root_block_device": cty.ListVal([]cty.Value{
  3095  					cty.ObjectVal(map[string]cty.Value{
  3096  						"volume_type": cty.StringVal("gp2"),
  3097  					}),
  3098  				}),
  3099  			}),
  3100  			After: cty.ObjectVal(map[string]cty.Value{
  3101  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3102  				"ami": cty.StringVal("ami-AFTER"),
  3103  				"disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  3104  					"mount_point": cty.String,
  3105  					"size":        cty.String,
  3106  				})),
  3107  				"root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  3108  					"volume_type": cty.String,
  3109  				})),
  3110  			}),
  3111  			RequiredReplace: cty.NewPathSet(),
  3112  			Schema:          testSchema(configschema.NestingList),
  3113  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3114    ~ resource "test_instance" "example" {
  3115        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3116        ~ disks = [
  3117            - {
  3118                - mount_point = "/var/diska" -> null
  3119                - size        = "50GB" -> null
  3120              },
  3121          ]
  3122          id    = "i-02ae66f368e8518a9"
  3123  
  3124        - root_block_device {
  3125            - volume_type = "gp2" -> null
  3126          }
  3127      }`,
  3128  		},
  3129  		"with dynamically-typed attribute": {
  3130  			Action: plans.Update,
  3131  			Mode:   addrs.ManagedResourceMode,
  3132  			Before: cty.ObjectVal(map[string]cty.Value{
  3133  				"block": cty.EmptyTupleVal,
  3134  			}),
  3135  			After: cty.ObjectVal(map[string]cty.Value{
  3136  				"block": cty.TupleVal([]cty.Value{
  3137  					cty.ObjectVal(map[string]cty.Value{
  3138  						"attr": cty.StringVal("foo"),
  3139  					}),
  3140  					cty.ObjectVal(map[string]cty.Value{
  3141  						"attr": cty.True,
  3142  					}),
  3143  				}),
  3144  			}),
  3145  			RequiredReplace: cty.NewPathSet(),
  3146  			Schema: &configschema.Block{
  3147  				BlockTypes: map[string]*configschema.NestedBlock{
  3148  					"block": {
  3149  						Block: configschema.Block{
  3150  							Attributes: map[string]*configschema.Attribute{
  3151  								"attr": {Type: cty.DynamicPseudoType, Optional: true},
  3152  							},
  3153  						},
  3154  						Nesting: configschema.NestingList,
  3155  					},
  3156  				},
  3157  			},
  3158  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3159    ~ resource "test_instance" "example" {
  3160        + block {
  3161            + attr = "foo"
  3162          }
  3163        + block {
  3164            + attr = true
  3165          }
  3166      }`,
  3167  		},
  3168  		"in-place sequence update - deletion": {
  3169  			Action: plans.Update,
  3170  			Mode:   addrs.ManagedResourceMode,
  3171  			Before: cty.ObjectVal(map[string]cty.Value{
  3172  				"list": cty.ListVal([]cty.Value{
  3173  					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("x")}),
  3174  					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}),
  3175  				}),
  3176  			}),
  3177  			After: cty.ObjectVal(map[string]cty.Value{
  3178  				"list": cty.ListVal([]cty.Value{
  3179  					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}),
  3180  					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("z")}),
  3181  				}),
  3182  			}),
  3183  			RequiredReplace: cty.NewPathSet(),
  3184  			Schema: &configschema.Block{
  3185  				BlockTypes: map[string]*configschema.NestedBlock{
  3186  					"list": {
  3187  						Block: configschema.Block{
  3188  							Attributes: map[string]*configschema.Attribute{
  3189  								"attr": {
  3190  									Type:     cty.String,
  3191  									Required: true,
  3192  								},
  3193  							},
  3194  						},
  3195  						Nesting: configschema.NestingList,
  3196  					},
  3197  				},
  3198  			},
  3199  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3200    ~ resource "test_instance" "example" {
  3201        ~ list {
  3202            ~ attr = "x" -> "y"
  3203          }
  3204        ~ list {
  3205            ~ attr = "y" -> "z"
  3206          }
  3207      }`,
  3208  		},
  3209  		"in-place update - unknown": {
  3210  			Action: plans.Update,
  3211  			Mode:   addrs.ManagedResourceMode,
  3212  			Before: cty.ObjectVal(map[string]cty.Value{
  3213  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3214  				"ami": cty.StringVal("ami-BEFORE"),
  3215  				"disks": cty.ListVal([]cty.Value{
  3216  					cty.ObjectVal(map[string]cty.Value{
  3217  						"mount_point": cty.StringVal("/var/diska"),
  3218  						"size":        cty.StringVal("50GB"),
  3219  					}),
  3220  				}),
  3221  				"root_block_device": cty.ListVal([]cty.Value{
  3222  					cty.ObjectVal(map[string]cty.Value{
  3223  						"volume_type": cty.StringVal("gp2"),
  3224  						"new_field":   cty.StringVal("new_value"),
  3225  					}),
  3226  				}),
  3227  			}),
  3228  			After: cty.ObjectVal(map[string]cty.Value{
  3229  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3230  				"ami": cty.StringVal("ami-AFTER"),
  3231  				"disks": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
  3232  					"mount_point": cty.String,
  3233  					"size":        cty.String,
  3234  				}))),
  3235  				"root_block_device": cty.ListVal([]cty.Value{
  3236  					cty.ObjectVal(map[string]cty.Value{
  3237  						"volume_type": cty.StringVal("gp2"),
  3238  						"new_field":   cty.StringVal("new_value"),
  3239  					}),
  3240  				}),
  3241  			}),
  3242  			RequiredReplace: cty.NewPathSet(),
  3243  			Schema:          testSchemaPlus(configschema.NestingList),
  3244  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3245    ~ resource "test_instance" "example" {
  3246        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3247        ~ disks = [
  3248            - {
  3249                - mount_point = "/var/diska" -> null
  3250                - size        = "50GB" -> null
  3251              },
  3252          ] -> (known after apply)
  3253          id    = "i-02ae66f368e8518a9"
  3254  
  3255          # (1 unchanged block hidden)
  3256      }`,
  3257  		},
  3258  		"in-place update - modification": {
  3259  			Action: plans.Update,
  3260  			Mode:   addrs.ManagedResourceMode,
  3261  			Before: cty.ObjectVal(map[string]cty.Value{
  3262  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3263  				"ami": cty.StringVal("ami-BEFORE"),
  3264  				"disks": cty.ListVal([]cty.Value{
  3265  					cty.ObjectVal(map[string]cty.Value{
  3266  						"mount_point": cty.StringVal("/var/diska"),
  3267  						"size":        cty.StringVal("50GB"),
  3268  					}),
  3269  					cty.ObjectVal(map[string]cty.Value{
  3270  						"mount_point": cty.StringVal("/var/diskb"),
  3271  						"size":        cty.StringVal("50GB"),
  3272  					}),
  3273  					cty.ObjectVal(map[string]cty.Value{
  3274  						"mount_point": cty.StringVal("/var/diskc"),
  3275  						"size":        cty.StringVal("50GB"),
  3276  					}),
  3277  				}),
  3278  				"root_block_device": cty.ListVal([]cty.Value{
  3279  					cty.ObjectVal(map[string]cty.Value{
  3280  						"volume_type": cty.StringVal("gp2"),
  3281  						"new_field":   cty.StringVal("new_value"),
  3282  					}),
  3283  				}),
  3284  			}),
  3285  			After: cty.ObjectVal(map[string]cty.Value{
  3286  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3287  				"ami": cty.StringVal("ami-AFTER"),
  3288  				"disks": cty.ListVal([]cty.Value{
  3289  					cty.ObjectVal(map[string]cty.Value{
  3290  						"mount_point": cty.StringVal("/var/diska"),
  3291  						"size":        cty.StringVal("50GB"),
  3292  					}),
  3293  					cty.ObjectVal(map[string]cty.Value{
  3294  						"mount_point": cty.StringVal("/var/diskb"),
  3295  						"size":        cty.StringVal("75GB"),
  3296  					}),
  3297  					cty.ObjectVal(map[string]cty.Value{
  3298  						"mount_point": cty.StringVal("/var/diskc"),
  3299  						"size":        cty.StringVal("25GB"),
  3300  					}),
  3301  				}),
  3302  				"root_block_device": cty.ListVal([]cty.Value{
  3303  					cty.ObjectVal(map[string]cty.Value{
  3304  						"volume_type": cty.StringVal("gp2"),
  3305  						"new_field":   cty.StringVal("new_value"),
  3306  					}),
  3307  				}),
  3308  			}),
  3309  			RequiredReplace: cty.NewPathSet(),
  3310  			Schema:          testSchemaPlus(configschema.NestingList),
  3311  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3312    ~ resource "test_instance" "example" {
  3313        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3314        ~ disks = [
  3315            ~ {
  3316                ~ size        = "50GB" -> "75GB"
  3317                  # (1 unchanged attribute hidden)
  3318              },
  3319            ~ {
  3320                ~ size        = "50GB" -> "25GB"
  3321                  # (1 unchanged attribute hidden)
  3322              },
  3323              # (1 unchanged element hidden)
  3324          ]
  3325          id    = "i-02ae66f368e8518a9"
  3326  
  3327          # (1 unchanged block hidden)
  3328      }`,
  3329  		},
  3330  	}
  3331  	runTestCases(t, testCases)
  3332  }
  3333  
  3334  func TestResourceChange_nestedSet(t *testing.T) {
  3335  	testCases := map[string]testCase{
  3336  		"creation from null - sensitive set": {
  3337  			Action: plans.Create,
  3338  			Mode:   addrs.ManagedResourceMode,
  3339  			Before: cty.NullVal(cty.Object(map[string]cty.Type{
  3340  				"id":  cty.String,
  3341  				"ami": cty.String,
  3342  				"disks": cty.Set(cty.Object(map[string]cty.Type{
  3343  					"mount_point": cty.String,
  3344  					"size":        cty.String,
  3345  				})),
  3346  				"root_block_device": cty.Set(cty.Object(map[string]cty.Type{
  3347  					"volume_type": cty.String,
  3348  				})),
  3349  			})),
  3350  			After: cty.ObjectVal(map[string]cty.Value{
  3351  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3352  				"ami": cty.StringVal("ami-AFTER"),
  3353  				"disks": cty.SetVal([]cty.Value{
  3354  					cty.ObjectVal(map[string]cty.Value{
  3355  						"mount_point": cty.StringVal("/var/diska"),
  3356  						"size":        cty.NullVal(cty.String),
  3357  					}),
  3358  				}),
  3359  				"root_block_device": cty.SetVal([]cty.Value{
  3360  					cty.ObjectVal(map[string]cty.Value{
  3361  						"volume_type": cty.StringVal("gp2"),
  3362  					}),
  3363  				}),
  3364  			}),
  3365  			AfterValMarks: []cty.PathValueMarks{
  3366  				{
  3367  					Path:  cty.Path{cty.GetAttrStep{Name: "disks"}},
  3368  					Marks: cty.NewValueMarks(marks.Sensitive),
  3369  				},
  3370  			},
  3371  			RequiredReplace: cty.NewPathSet(),
  3372  			Schema:          testSchema(configschema.NestingSet),
  3373  			ExpectedOutput: `  # test_instance.example will be created
  3374    + resource "test_instance" "example" {
  3375        + ami   = "ami-AFTER"
  3376        + disks = (sensitive value)
  3377        + id    = "i-02ae66f368e8518a9"
  3378  
  3379        + root_block_device {
  3380            + volume_type = "gp2"
  3381          }
  3382      }`,
  3383  		},
  3384  		"in-place update - creation": {
  3385  			Action: plans.Update,
  3386  			Mode:   addrs.ManagedResourceMode,
  3387  			Before: cty.ObjectVal(map[string]cty.Value{
  3388  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3389  				"ami": cty.StringVal("ami-BEFORE"),
  3390  				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3391  					"mount_point": cty.String,
  3392  					"size":        cty.String,
  3393  				})),
  3394  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3395  					"volume_type": cty.String,
  3396  				})),
  3397  			}),
  3398  			After: cty.ObjectVal(map[string]cty.Value{
  3399  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3400  				"ami": cty.StringVal("ami-AFTER"),
  3401  				"disks": cty.SetVal([]cty.Value{
  3402  					cty.ObjectVal(map[string]cty.Value{
  3403  						"mount_point": cty.StringVal("/var/diska"),
  3404  						"size":        cty.NullVal(cty.String),
  3405  					}),
  3406  				}),
  3407  				"root_block_device": cty.SetVal([]cty.Value{
  3408  					cty.ObjectVal(map[string]cty.Value{
  3409  						"volume_type": cty.StringVal("gp2"),
  3410  					}),
  3411  				}),
  3412  			}),
  3413  			RequiredReplace: cty.NewPathSet(),
  3414  			Schema:          testSchema(configschema.NestingSet),
  3415  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3416    ~ resource "test_instance" "example" {
  3417        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3418        ~ disks = [
  3419            + {
  3420                + mount_point = "/var/diska"
  3421              },
  3422          ]
  3423          id    = "i-02ae66f368e8518a9"
  3424  
  3425        + root_block_device {
  3426            + volume_type = "gp2"
  3427          }
  3428      }`,
  3429  		},
  3430  		"in-place update - creation - sensitive set": {
  3431  			Action: plans.Update,
  3432  			Mode:   addrs.ManagedResourceMode,
  3433  			Before: cty.ObjectVal(map[string]cty.Value{
  3434  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3435  				"ami": cty.StringVal("ami-BEFORE"),
  3436  				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3437  					"mount_point": cty.String,
  3438  					"size":        cty.String,
  3439  				})),
  3440  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3441  					"volume_type": cty.String,
  3442  				})),
  3443  			}),
  3444  			After: cty.ObjectVal(map[string]cty.Value{
  3445  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3446  				"ami": cty.StringVal("ami-AFTER"),
  3447  				"disks": cty.SetVal([]cty.Value{
  3448  					cty.ObjectVal(map[string]cty.Value{
  3449  						"mount_point": cty.StringVal("/var/diska"),
  3450  						"size":        cty.NullVal(cty.String),
  3451  					}),
  3452  				}),
  3453  				"root_block_device": cty.SetVal([]cty.Value{
  3454  					cty.ObjectVal(map[string]cty.Value{
  3455  						"volume_type": cty.StringVal("gp2"),
  3456  					}),
  3457  				}),
  3458  			}),
  3459  			AfterValMarks: []cty.PathValueMarks{
  3460  				{
  3461  					Path:  cty.Path{cty.GetAttrStep{Name: "disks"}},
  3462  					Marks: cty.NewValueMarks(marks.Sensitive),
  3463  				},
  3464  			},
  3465  			RequiredReplace: cty.NewPathSet(),
  3466  			Schema:          testSchema(configschema.NestingSet),
  3467  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3468    ~ resource "test_instance" "example" {
  3469        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3470        # Warning: this attribute value will be marked as sensitive and will not
  3471        # display in UI output after applying this change.
  3472        ~ disks = (sensitive value)
  3473          id    = "i-02ae66f368e8518a9"
  3474  
  3475        + root_block_device {
  3476            + volume_type = "gp2"
  3477          }
  3478      }`,
  3479  		},
  3480  		"in-place update - marking set sensitive": {
  3481  			Action: plans.Update,
  3482  			Mode:   addrs.ManagedResourceMode,
  3483  			Before: cty.ObjectVal(map[string]cty.Value{
  3484  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3485  				"ami": cty.StringVal("ami-BEFORE"),
  3486  				"disks": cty.SetVal([]cty.Value{
  3487  					cty.ObjectVal(map[string]cty.Value{
  3488  						"mount_point": cty.StringVal("/var/diska"),
  3489  						"size":        cty.StringVal("50GB"),
  3490  					}),
  3491  				}),
  3492  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3493  					"volume_type": cty.String,
  3494  				})),
  3495  			}),
  3496  			After: cty.ObjectVal(map[string]cty.Value{
  3497  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3498  				"ami": cty.StringVal("ami-AFTER"),
  3499  				"disks": cty.SetVal([]cty.Value{
  3500  					cty.ObjectVal(map[string]cty.Value{
  3501  						"mount_point": cty.StringVal("/var/diska"),
  3502  						"size":        cty.StringVal("50GB"),
  3503  					}),
  3504  				}),
  3505  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3506  					"volume_type": cty.String,
  3507  				})),
  3508  			}),
  3509  			AfterValMarks: []cty.PathValueMarks{
  3510  				{
  3511  					Path:  cty.Path{cty.GetAttrStep{Name: "disks"}},
  3512  					Marks: cty.NewValueMarks(marks.Sensitive),
  3513  				},
  3514  			},
  3515  			RequiredReplace: cty.NewPathSet(),
  3516  			Schema:          testSchema(configschema.NestingSet),
  3517  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3518    ~ resource "test_instance" "example" {
  3519        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3520        # Warning: this attribute value will be marked as sensitive and will not
  3521        # display in UI output after applying this change. The value is unchanged.
  3522        ~ disks = (sensitive value)
  3523          id    = "i-02ae66f368e8518a9"
  3524      }`,
  3525  		},
  3526  		"in-place update - insertion": {
  3527  			Action: plans.Update,
  3528  			Mode:   addrs.ManagedResourceMode,
  3529  			Before: cty.ObjectVal(map[string]cty.Value{
  3530  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3531  				"ami": cty.StringVal("ami-BEFORE"),
  3532  				"disks": cty.SetVal([]cty.Value{
  3533  					cty.ObjectVal(map[string]cty.Value{
  3534  						"mount_point": cty.StringVal("/var/diska"),
  3535  						"size":        cty.NullVal(cty.String),
  3536  					}),
  3537  					cty.ObjectVal(map[string]cty.Value{
  3538  						"mount_point": cty.StringVal("/var/diskb"),
  3539  						"size":        cty.StringVal("100GB"),
  3540  					}),
  3541  				}),
  3542  				"root_block_device": cty.SetVal([]cty.Value{
  3543  					cty.ObjectVal(map[string]cty.Value{
  3544  						"volume_type": cty.StringVal("gp2"),
  3545  						"new_field":   cty.NullVal(cty.String),
  3546  					}),
  3547  				}),
  3548  			}),
  3549  			After: cty.ObjectVal(map[string]cty.Value{
  3550  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3551  				"ami": cty.StringVal("ami-AFTER"),
  3552  				"disks": cty.SetVal([]cty.Value{
  3553  					cty.ObjectVal(map[string]cty.Value{
  3554  						"mount_point": cty.StringVal("/var/diska"),
  3555  						"size":        cty.StringVal("50GB"),
  3556  					}),
  3557  					cty.ObjectVal(map[string]cty.Value{
  3558  						"mount_point": cty.StringVal("/var/diskb"),
  3559  						"size":        cty.StringVal("100GB"),
  3560  					}),
  3561  				}),
  3562  				"root_block_device": cty.SetVal([]cty.Value{
  3563  					cty.ObjectVal(map[string]cty.Value{
  3564  						"volume_type": cty.StringVal("gp2"),
  3565  						"new_field":   cty.StringVal("new_value"),
  3566  					}),
  3567  				}),
  3568  			}),
  3569  			RequiredReplace: cty.NewPathSet(),
  3570  			Schema:          testSchemaPlus(configschema.NestingSet),
  3571  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3572    ~ resource "test_instance" "example" {
  3573        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3574        ~ disks = [
  3575            - {
  3576                - mount_point = "/var/diska" -> null
  3577              },
  3578            + {
  3579                + mount_point = "/var/diska"
  3580                + size        = "50GB"
  3581              },
  3582              # (1 unchanged element hidden)
  3583          ]
  3584          id    = "i-02ae66f368e8518a9"
  3585  
  3586        - root_block_device {
  3587            - volume_type = "gp2" -> null
  3588          }
  3589        + root_block_device {
  3590            + new_field   = "new_value"
  3591            + volume_type = "gp2"
  3592          }
  3593      }`,
  3594  		},
  3595  		"force-new update (whole block)": {
  3596  			Action:       plans.DeleteThenCreate,
  3597  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  3598  			Mode:         addrs.ManagedResourceMode,
  3599  			Before: cty.ObjectVal(map[string]cty.Value{
  3600  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3601  				"ami": cty.StringVal("ami-BEFORE"),
  3602  				"root_block_device": cty.SetVal([]cty.Value{
  3603  					cty.ObjectVal(map[string]cty.Value{
  3604  						"volume_type": cty.StringVal("gp2"),
  3605  					}),
  3606  				}),
  3607  				"disks": cty.SetVal([]cty.Value{
  3608  					cty.ObjectVal(map[string]cty.Value{
  3609  						"mount_point": cty.StringVal("/var/diska"),
  3610  						"size":        cty.StringVal("50GB"),
  3611  					}),
  3612  				}),
  3613  			}),
  3614  			After: cty.ObjectVal(map[string]cty.Value{
  3615  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3616  				"ami": cty.StringVal("ami-AFTER"),
  3617  				"root_block_device": cty.SetVal([]cty.Value{
  3618  					cty.ObjectVal(map[string]cty.Value{
  3619  						"volume_type": cty.StringVal("different"),
  3620  					}),
  3621  				}),
  3622  				"disks": cty.SetVal([]cty.Value{
  3623  					cty.ObjectVal(map[string]cty.Value{
  3624  						"mount_point": cty.StringVal("/var/diskb"),
  3625  						"size":        cty.StringVal("50GB"),
  3626  					}),
  3627  				}),
  3628  			}),
  3629  			RequiredReplace: cty.NewPathSet(
  3630  				cty.Path{cty.GetAttrStep{Name: "root_block_device"}},
  3631  				cty.Path{cty.GetAttrStep{Name: "disks"}},
  3632  			),
  3633  			Schema: testSchema(configschema.NestingSet),
  3634  			ExpectedOutput: `  # test_instance.example must be replaced
  3635  -/+ resource "test_instance" "example" {
  3636        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3637        ~ disks = [
  3638            - { # forces replacement
  3639                - mount_point = "/var/diska" -> null
  3640                - size        = "50GB" -> null
  3641              },
  3642            + { # forces replacement
  3643                + mount_point = "/var/diskb"
  3644                + size        = "50GB"
  3645              },
  3646          ]
  3647          id    = "i-02ae66f368e8518a9"
  3648  
  3649        - root_block_device { # forces replacement
  3650            - volume_type = "gp2" -> null
  3651          }
  3652        + root_block_device { # forces replacement
  3653            + volume_type = "different"
  3654          }
  3655      }`,
  3656  		},
  3657  		"in-place update - deletion": {
  3658  			Action: plans.Update,
  3659  			Mode:   addrs.ManagedResourceMode,
  3660  			Before: cty.ObjectVal(map[string]cty.Value{
  3661  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3662  				"ami": cty.StringVal("ami-BEFORE"),
  3663  				"root_block_device": cty.SetVal([]cty.Value{
  3664  					cty.ObjectVal(map[string]cty.Value{
  3665  						"volume_type": cty.StringVal("gp2"),
  3666  						"new_field":   cty.StringVal("new_value"),
  3667  					}),
  3668  				}),
  3669  				"disks": cty.SetVal([]cty.Value{
  3670  					cty.ObjectVal(map[string]cty.Value{
  3671  						"mount_point": cty.StringVal("/var/diska"),
  3672  						"size":        cty.StringVal("50GB"),
  3673  					}),
  3674  				}),
  3675  			}),
  3676  			After: cty.ObjectVal(map[string]cty.Value{
  3677  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3678  				"ami": cty.StringVal("ami-AFTER"),
  3679  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3680  					"volume_type": cty.String,
  3681  					"new_field":   cty.String,
  3682  				})),
  3683  				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3684  					"mount_point": cty.String,
  3685  					"size":        cty.String,
  3686  				})),
  3687  			}),
  3688  			RequiredReplace: cty.NewPathSet(),
  3689  			Schema:          testSchemaPlus(configschema.NestingSet),
  3690  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3691    ~ resource "test_instance" "example" {
  3692        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3693        ~ disks = [
  3694            - {
  3695                - mount_point = "/var/diska" -> null
  3696                - size        = "50GB" -> null
  3697              },
  3698          ]
  3699          id    = "i-02ae66f368e8518a9"
  3700  
  3701        - root_block_device {
  3702            - new_field   = "new_value" -> null
  3703            - volume_type = "gp2" -> null
  3704          }
  3705      }`,
  3706  		},
  3707  		"in-place update - empty nested sets": {
  3708  			Action: plans.Update,
  3709  			Mode:   addrs.ManagedResourceMode,
  3710  			Before: cty.ObjectVal(map[string]cty.Value{
  3711  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3712  				"ami": cty.StringVal("ami-BEFORE"),
  3713  				"disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
  3714  					"mount_point": cty.String,
  3715  					"size":        cty.String,
  3716  				}))),
  3717  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3718  					"volume_type": cty.String,
  3719  				})),
  3720  			}),
  3721  			After: cty.ObjectVal(map[string]cty.Value{
  3722  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3723  				"ami": cty.StringVal("ami-AFTER"),
  3724  				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3725  					"mount_point": cty.String,
  3726  					"size":        cty.String,
  3727  				})),
  3728  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3729  					"volume_type": cty.String,
  3730  				})),
  3731  			}),
  3732  			RequiredReplace: cty.NewPathSet(),
  3733  			Schema:          testSchema(configschema.NestingSet),
  3734  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3735    ~ resource "test_instance" "example" {
  3736        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3737        + disks = []
  3738          id    = "i-02ae66f368e8518a9"
  3739      }`,
  3740  		},
  3741  		"in-place update - null insertion": {
  3742  			Action: plans.Update,
  3743  			Mode:   addrs.ManagedResourceMode,
  3744  			Before: cty.ObjectVal(map[string]cty.Value{
  3745  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3746  				"ami": cty.StringVal("ami-BEFORE"),
  3747  				"disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
  3748  					"mount_point": cty.String,
  3749  					"size":        cty.String,
  3750  				}))),
  3751  				"root_block_device": cty.SetVal([]cty.Value{
  3752  					cty.ObjectVal(map[string]cty.Value{
  3753  						"volume_type": cty.StringVal("gp2"),
  3754  						"new_field":   cty.NullVal(cty.String),
  3755  					}),
  3756  				}),
  3757  			}),
  3758  			After: cty.ObjectVal(map[string]cty.Value{
  3759  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3760  				"ami": cty.StringVal("ami-AFTER"),
  3761  				"disks": cty.SetVal([]cty.Value{
  3762  					cty.ObjectVal(map[string]cty.Value{
  3763  						"mount_point": cty.StringVal("/var/diska"),
  3764  						"size":        cty.StringVal("50GB"),
  3765  					}),
  3766  				}),
  3767  				"root_block_device": cty.SetVal([]cty.Value{
  3768  					cty.ObjectVal(map[string]cty.Value{
  3769  						"volume_type": cty.StringVal("gp2"),
  3770  						"new_field":   cty.StringVal("new_value"),
  3771  					}),
  3772  				}),
  3773  			}),
  3774  			RequiredReplace: cty.NewPathSet(),
  3775  			Schema:          testSchemaPlus(configschema.NestingSet),
  3776  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3777    ~ resource "test_instance" "example" {
  3778        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3779        + disks = [
  3780            + {
  3781                + mount_point = "/var/diska"
  3782                + size        = "50GB"
  3783              },
  3784          ]
  3785          id    = "i-02ae66f368e8518a9"
  3786  
  3787        - root_block_device {
  3788            - volume_type = "gp2" -> null
  3789          }
  3790        + root_block_device {
  3791            + new_field   = "new_value"
  3792            + volume_type = "gp2"
  3793          }
  3794      }`,
  3795  		},
  3796  		"in-place update - unknown": {
  3797  			Action: plans.Update,
  3798  			Mode:   addrs.ManagedResourceMode,
  3799  			Before: cty.ObjectVal(map[string]cty.Value{
  3800  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3801  				"ami": cty.StringVal("ami-BEFORE"),
  3802  				"disks": cty.SetVal([]cty.Value{
  3803  					cty.ObjectVal(map[string]cty.Value{
  3804  						"mount_point": cty.StringVal("/var/diska"),
  3805  						"size":        cty.StringVal("50GB"),
  3806  					}),
  3807  				}),
  3808  				"root_block_device": cty.SetVal([]cty.Value{
  3809  					cty.ObjectVal(map[string]cty.Value{
  3810  						"volume_type": cty.StringVal("gp2"),
  3811  						"new_field":   cty.StringVal("new_value"),
  3812  					}),
  3813  				}),
  3814  			}),
  3815  			After: cty.ObjectVal(map[string]cty.Value{
  3816  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3817  				"ami": cty.StringVal("ami-AFTER"),
  3818  				"disks": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
  3819  					"mount_point": cty.String,
  3820  					"size":        cty.String,
  3821  				}))),
  3822  				"root_block_device": cty.SetVal([]cty.Value{
  3823  					cty.ObjectVal(map[string]cty.Value{
  3824  						"volume_type": cty.StringVal("gp2"),
  3825  						"new_field":   cty.StringVal("new_value"),
  3826  					}),
  3827  				}),
  3828  			}),
  3829  			RequiredReplace: cty.NewPathSet(),
  3830  			Schema:          testSchemaPlus(configschema.NestingSet),
  3831  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3832    ~ resource "test_instance" "example" {
  3833        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3834        ~ disks = [
  3835            - {
  3836                - mount_point = "/var/diska" -> null
  3837                - size        = "50GB" -> null
  3838              },
  3839          ] -> (known after apply)
  3840          id    = "i-02ae66f368e8518a9"
  3841  
  3842          # (1 unchanged block hidden)
  3843      }`,
  3844  		},
  3845  	}
  3846  	runTestCases(t, testCases)
  3847  }
  3848  
  3849  func TestResourceChange_nestedMap(t *testing.T) {
  3850  	testCases := map[string]testCase{
  3851  		"creation from null": {
  3852  			Action: plans.Update,
  3853  			Mode:   addrs.ManagedResourceMode,
  3854  			Before: cty.ObjectVal(map[string]cty.Value{
  3855  				"id":  cty.NullVal(cty.String),
  3856  				"ami": cty.NullVal(cty.String),
  3857  				"disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
  3858  					"mount_point": cty.String,
  3859  					"size":        cty.String,
  3860  				}))),
  3861  				"root_block_device": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
  3862  					"volume_type": cty.String,
  3863  				}))),
  3864  			}),
  3865  			After: cty.ObjectVal(map[string]cty.Value{
  3866  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3867  				"ami": cty.StringVal("ami-AFTER"),
  3868  				"disks": cty.MapVal(map[string]cty.Value{
  3869  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3870  						"mount_point": cty.StringVal("/var/diska"),
  3871  						"size":        cty.NullVal(cty.String),
  3872  					}),
  3873  				}),
  3874  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3875  					"a": cty.ObjectVal(map[string]cty.Value{
  3876  						"volume_type": cty.StringVal("gp2"),
  3877  					}),
  3878  				}),
  3879  			}),
  3880  			RequiredReplace: cty.NewPathSet(),
  3881  			Schema:          testSchema(configschema.NestingMap),
  3882  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3883    ~ resource "test_instance" "example" {
  3884        + ami   = "ami-AFTER"
  3885        + disks = {
  3886            + "disk_a" = {
  3887                + mount_point = "/var/diska"
  3888              },
  3889          }
  3890        + id    = "i-02ae66f368e8518a9"
  3891  
  3892        + root_block_device "a" {
  3893            + volume_type = "gp2"
  3894          }
  3895      }`,
  3896  		},
  3897  		"in-place update - creation": {
  3898  			Action: plans.Update,
  3899  			Mode:   addrs.ManagedResourceMode,
  3900  			Before: cty.ObjectVal(map[string]cty.Value{
  3901  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3902  				"ami": cty.StringVal("ami-BEFORE"),
  3903  				"disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  3904  					"mount_point": cty.String,
  3905  					"size":        cty.String,
  3906  				})),
  3907  				"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  3908  					"volume_type": cty.String,
  3909  				})),
  3910  			}),
  3911  			After: cty.ObjectVal(map[string]cty.Value{
  3912  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3913  				"ami": cty.StringVal("ami-AFTER"),
  3914  				"disks": cty.MapVal(map[string]cty.Value{
  3915  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3916  						"mount_point": cty.StringVal("/var/diska"),
  3917  						"size":        cty.NullVal(cty.String),
  3918  					}),
  3919  				}),
  3920  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3921  					"a": cty.ObjectVal(map[string]cty.Value{
  3922  						"volume_type": cty.StringVal("gp2"),
  3923  					}),
  3924  				}),
  3925  			}),
  3926  			RequiredReplace: cty.NewPathSet(),
  3927  			Schema:          testSchema(configschema.NestingMap),
  3928  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3929    ~ resource "test_instance" "example" {
  3930        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3931        ~ disks = {
  3932            + "disk_a" = {
  3933                + mount_point = "/var/diska"
  3934              },
  3935          }
  3936          id    = "i-02ae66f368e8518a9"
  3937  
  3938        + root_block_device "a" {
  3939            + volume_type = "gp2"
  3940          }
  3941      }`,
  3942  		},
  3943  		"in-place update - change attr": {
  3944  			Action: plans.Update,
  3945  			Mode:   addrs.ManagedResourceMode,
  3946  			Before: cty.ObjectVal(map[string]cty.Value{
  3947  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3948  				"ami": cty.StringVal("ami-BEFORE"),
  3949  				"disks": cty.MapVal(map[string]cty.Value{
  3950  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3951  						"mount_point": cty.StringVal("/var/diska"),
  3952  						"size":        cty.NullVal(cty.String),
  3953  					}),
  3954  				}),
  3955  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3956  					"a": cty.ObjectVal(map[string]cty.Value{
  3957  						"volume_type": cty.StringVal("gp2"),
  3958  						"new_field":   cty.NullVal(cty.String),
  3959  					}),
  3960  				}),
  3961  			}),
  3962  			After: cty.ObjectVal(map[string]cty.Value{
  3963  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3964  				"ami": cty.StringVal("ami-AFTER"),
  3965  				"disks": cty.MapVal(map[string]cty.Value{
  3966  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3967  						"mount_point": cty.StringVal("/var/diska"),
  3968  						"size":        cty.StringVal("50GB"),
  3969  					}),
  3970  				}),
  3971  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3972  					"a": cty.ObjectVal(map[string]cty.Value{
  3973  						"volume_type": cty.StringVal("gp2"),
  3974  						"new_field":   cty.StringVal("new_value"),
  3975  					}),
  3976  				}),
  3977  			}),
  3978  			RequiredReplace: cty.NewPathSet(),
  3979  			Schema:          testSchemaPlus(configschema.NestingMap),
  3980  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3981    ~ resource "test_instance" "example" {
  3982        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3983        ~ disks = {
  3984            ~ "disk_a" = {
  3985                + size        = "50GB"
  3986                  # (1 unchanged attribute hidden)
  3987              },
  3988          }
  3989          id    = "i-02ae66f368e8518a9"
  3990  
  3991        ~ root_block_device "a" {
  3992            + new_field   = "new_value"
  3993              # (1 unchanged attribute hidden)
  3994          }
  3995      }`,
  3996  		},
  3997  		"in-place update - insertion": {
  3998  			Action: plans.Update,
  3999  			Mode:   addrs.ManagedResourceMode,
  4000  			Before: cty.ObjectVal(map[string]cty.Value{
  4001  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4002  				"ami": cty.StringVal("ami-BEFORE"),
  4003  				"disks": cty.MapVal(map[string]cty.Value{
  4004  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4005  						"mount_point": cty.StringVal("/var/diska"),
  4006  						"size":        cty.StringVal("50GB"),
  4007  					}),
  4008  				}),
  4009  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4010  					"a": cty.ObjectVal(map[string]cty.Value{
  4011  						"volume_type": cty.StringVal("gp2"),
  4012  						"new_field":   cty.NullVal(cty.String),
  4013  					}),
  4014  				}),
  4015  			}),
  4016  			After: cty.ObjectVal(map[string]cty.Value{
  4017  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4018  				"ami": cty.StringVal("ami-AFTER"),
  4019  				"disks": cty.MapVal(map[string]cty.Value{
  4020  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4021  						"mount_point": cty.StringVal("/var/diska"),
  4022  						"size":        cty.StringVal("50GB"),
  4023  					}),
  4024  					"disk_2": cty.ObjectVal(map[string]cty.Value{
  4025  						"mount_point": cty.StringVal("/var/disk2"),
  4026  						"size":        cty.StringVal("50GB"),
  4027  					}),
  4028  				}),
  4029  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4030  					"a": cty.ObjectVal(map[string]cty.Value{
  4031  						"volume_type": cty.StringVal("gp2"),
  4032  						"new_field":   cty.NullVal(cty.String),
  4033  					}),
  4034  					"b": cty.ObjectVal(map[string]cty.Value{
  4035  						"volume_type": cty.StringVal("gp2"),
  4036  						"new_field":   cty.StringVal("new_value"),
  4037  					}),
  4038  				}),
  4039  			}),
  4040  			RequiredReplace: cty.NewPathSet(),
  4041  			Schema:          testSchemaPlus(configschema.NestingMap),
  4042  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4043    ~ resource "test_instance" "example" {
  4044        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4045        ~ disks = {
  4046            + "disk_2" = {
  4047                + mount_point = "/var/disk2"
  4048                + size        = "50GB"
  4049              },
  4050              # (1 unchanged element hidden)
  4051          }
  4052          id    = "i-02ae66f368e8518a9"
  4053  
  4054        + root_block_device "b" {
  4055            + new_field   = "new_value"
  4056            + volume_type = "gp2"
  4057          }
  4058  
  4059          # (1 unchanged block hidden)
  4060      }`,
  4061  		},
  4062  		"force-new update (whole block)": {
  4063  			Action:       plans.DeleteThenCreate,
  4064  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  4065  			Mode:         addrs.ManagedResourceMode,
  4066  			Before: cty.ObjectVal(map[string]cty.Value{
  4067  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4068  				"ami": cty.StringVal("ami-BEFORE"),
  4069  				"disks": cty.MapVal(map[string]cty.Value{
  4070  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4071  						"mount_point": cty.StringVal("/var/diska"),
  4072  						"size":        cty.StringVal("50GB"),
  4073  					}),
  4074  				}),
  4075  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4076  					"a": cty.ObjectVal(map[string]cty.Value{
  4077  						"volume_type": cty.StringVal("gp2"),
  4078  					}),
  4079  					"b": cty.ObjectVal(map[string]cty.Value{
  4080  						"volume_type": cty.StringVal("standard"),
  4081  					}),
  4082  				}),
  4083  			}),
  4084  			After: cty.ObjectVal(map[string]cty.Value{
  4085  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4086  				"ami": cty.StringVal("ami-AFTER"),
  4087  				"disks": cty.MapVal(map[string]cty.Value{
  4088  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4089  						"mount_point": cty.StringVal("/var/diska"),
  4090  						"size":        cty.StringVal("100GB"),
  4091  					}),
  4092  				}),
  4093  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4094  					"a": cty.ObjectVal(map[string]cty.Value{
  4095  						"volume_type": cty.StringVal("different"),
  4096  					}),
  4097  					"b": cty.ObjectVal(map[string]cty.Value{
  4098  						"volume_type": cty.StringVal("standard"),
  4099  					}),
  4100  				}),
  4101  			}),
  4102  			RequiredReplace: cty.NewPathSet(cty.Path{
  4103  				cty.GetAttrStep{Name: "root_block_device"},
  4104  				cty.IndexStep{Key: cty.StringVal("a")},
  4105  			},
  4106  				cty.Path{cty.GetAttrStep{Name: "disks"}},
  4107  			),
  4108  			Schema: testSchema(configschema.NestingMap),
  4109  			ExpectedOutput: `  # test_instance.example must be replaced
  4110  -/+ resource "test_instance" "example" {
  4111        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4112        ~ disks = {
  4113            ~ "disk_a" = { # forces replacement
  4114                ~ size        = "50GB" -> "100GB"
  4115                  # (1 unchanged attribute hidden)
  4116              },
  4117          }
  4118          id    = "i-02ae66f368e8518a9"
  4119  
  4120        ~ root_block_device "a" { # forces replacement
  4121            ~ volume_type = "gp2" -> "different"
  4122          }
  4123  
  4124          # (1 unchanged block hidden)
  4125      }`,
  4126  		},
  4127  		"in-place update - deletion": {
  4128  			Action: plans.Update,
  4129  			Mode:   addrs.ManagedResourceMode,
  4130  			Before: cty.ObjectVal(map[string]cty.Value{
  4131  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4132  				"ami": cty.StringVal("ami-BEFORE"),
  4133  				"disks": cty.MapVal(map[string]cty.Value{
  4134  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4135  						"mount_point": cty.StringVal("/var/diska"),
  4136  						"size":        cty.StringVal("50GB"),
  4137  					}),
  4138  				}),
  4139  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4140  					"a": cty.ObjectVal(map[string]cty.Value{
  4141  						"volume_type": cty.StringVal("gp2"),
  4142  						"new_field":   cty.StringVal("new_value"),
  4143  					}),
  4144  				}),
  4145  			}),
  4146  			After: cty.ObjectVal(map[string]cty.Value{
  4147  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4148  				"ami": cty.StringVal("ami-AFTER"),
  4149  				"disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  4150  					"mount_point": cty.String,
  4151  					"size":        cty.String,
  4152  				})),
  4153  				"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  4154  					"volume_type": cty.String,
  4155  					"new_field":   cty.String,
  4156  				})),
  4157  			}),
  4158  			RequiredReplace: cty.NewPathSet(),
  4159  			Schema:          testSchemaPlus(configschema.NestingMap),
  4160  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4161    ~ resource "test_instance" "example" {
  4162        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4163        ~ disks = {
  4164            - "disk_a" = {
  4165                - mount_point = "/var/diska" -> null
  4166                - size        = "50GB" -> null
  4167              },
  4168          }
  4169          id    = "i-02ae66f368e8518a9"
  4170  
  4171        - root_block_device "a" {
  4172            - new_field   = "new_value" -> null
  4173            - volume_type = "gp2" -> null
  4174          }
  4175      }`,
  4176  		},
  4177  		"in-place update - unknown": {
  4178  			Action: plans.Update,
  4179  			Mode:   addrs.ManagedResourceMode,
  4180  			Before: cty.ObjectVal(map[string]cty.Value{
  4181  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4182  				"ami": cty.StringVal("ami-BEFORE"),
  4183  				"disks": cty.MapVal(map[string]cty.Value{
  4184  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4185  						"mount_point": cty.StringVal("/var/diska"),
  4186  						"size":        cty.StringVal("50GB"),
  4187  					}),
  4188  				}),
  4189  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4190  					"a": cty.ObjectVal(map[string]cty.Value{
  4191  						"volume_type": cty.StringVal("gp2"),
  4192  						"new_field":   cty.StringVal("new_value"),
  4193  					}),
  4194  				}),
  4195  			}),
  4196  			After: cty.ObjectVal(map[string]cty.Value{
  4197  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4198  				"ami": cty.StringVal("ami-AFTER"),
  4199  				"disks": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{
  4200  					"mount_point": cty.String,
  4201  					"size":        cty.String,
  4202  				}))),
  4203  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4204  					"a": cty.ObjectVal(map[string]cty.Value{
  4205  						"volume_type": cty.StringVal("gp2"),
  4206  						"new_field":   cty.StringVal("new_value"),
  4207  					}),
  4208  				}),
  4209  			}),
  4210  			RequiredReplace: cty.NewPathSet(),
  4211  			Schema:          testSchemaPlus(configschema.NestingMap),
  4212  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4213    ~ resource "test_instance" "example" {
  4214        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4215        ~ disks = {
  4216            - "disk_a" = {
  4217                - mount_point = "/var/diska" -> null
  4218                - size        = "50GB" -> null
  4219              },
  4220          } -> (known after apply)
  4221          id    = "i-02ae66f368e8518a9"
  4222  
  4223          # (1 unchanged block hidden)
  4224      }`,
  4225  		},
  4226  		"in-place update - insertion sensitive": {
  4227  			Action: plans.Update,
  4228  			Mode:   addrs.ManagedResourceMode,
  4229  			Before: cty.ObjectVal(map[string]cty.Value{
  4230  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4231  				"ami": cty.StringVal("ami-BEFORE"),
  4232  				"disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  4233  					"mount_point": cty.String,
  4234  					"size":        cty.String,
  4235  				})),
  4236  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4237  					"a": cty.ObjectVal(map[string]cty.Value{
  4238  						"volume_type": cty.StringVal("gp2"),
  4239  						"new_field":   cty.StringVal("new_value"),
  4240  					}),
  4241  				}),
  4242  			}),
  4243  			After: cty.ObjectVal(map[string]cty.Value{
  4244  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4245  				"ami": cty.StringVal("ami-AFTER"),
  4246  				"disks": cty.MapVal(map[string]cty.Value{
  4247  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4248  						"mount_point": cty.StringVal("/var/diska"),
  4249  						"size":        cty.StringVal("50GB"),
  4250  					}),
  4251  				}),
  4252  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4253  					"a": cty.ObjectVal(map[string]cty.Value{
  4254  						"volume_type": cty.StringVal("gp2"),
  4255  						"new_field":   cty.StringVal("new_value"),
  4256  					}),
  4257  				}),
  4258  			}),
  4259  			AfterValMarks: []cty.PathValueMarks{
  4260  				{
  4261  					Path: cty.Path{cty.GetAttrStep{Name: "disks"},
  4262  						cty.IndexStep{Key: cty.StringVal("disk_a")},
  4263  						cty.GetAttrStep{Name: "mount_point"},
  4264  					},
  4265  					Marks: cty.NewValueMarks(marks.Sensitive),
  4266  				},
  4267  			},
  4268  			RequiredReplace: cty.NewPathSet(),
  4269  			Schema:          testSchemaPlus(configschema.NestingMap),
  4270  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4271    ~ resource "test_instance" "example" {
  4272        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4273        ~ disks = {
  4274            + "disk_a" = {
  4275                + mount_point = (sensitive value)
  4276                + size        = "50GB"
  4277              },
  4278          }
  4279          id    = "i-02ae66f368e8518a9"
  4280  
  4281          # (1 unchanged block hidden)
  4282      }`,
  4283  		},
  4284  		"in-place update - multiple unchanged blocks": {
  4285  			Action: plans.Update,
  4286  			Mode:   addrs.ManagedResourceMode,
  4287  			Before: cty.ObjectVal(map[string]cty.Value{
  4288  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4289  				"ami": cty.StringVal("ami-BEFORE"),
  4290  				"disks": cty.MapVal(map[string]cty.Value{
  4291  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4292  						"mount_point": cty.StringVal("/var/diska"),
  4293  						"size":        cty.StringVal("50GB"),
  4294  					}),
  4295  				}),
  4296  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4297  					"a": cty.ObjectVal(map[string]cty.Value{
  4298  						"volume_type": cty.StringVal("gp2"),
  4299  					}),
  4300  					"b": cty.ObjectVal(map[string]cty.Value{
  4301  						"volume_type": cty.StringVal("gp2"),
  4302  					}),
  4303  				}),
  4304  			}),
  4305  			After: cty.ObjectVal(map[string]cty.Value{
  4306  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4307  				"ami": cty.StringVal("ami-AFTER"),
  4308  				"disks": cty.MapVal(map[string]cty.Value{
  4309  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4310  						"mount_point": cty.StringVal("/var/diska"),
  4311  						"size":        cty.StringVal("50GB"),
  4312  					}),
  4313  				}),
  4314  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4315  					"a": cty.ObjectVal(map[string]cty.Value{
  4316  						"volume_type": cty.StringVal("gp2"),
  4317  					}),
  4318  					"b": cty.ObjectVal(map[string]cty.Value{
  4319  						"volume_type": cty.StringVal("gp2"),
  4320  					}),
  4321  				}),
  4322  			}),
  4323  			RequiredReplace: cty.NewPathSet(),
  4324  			Schema:          testSchema(configschema.NestingMap),
  4325  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4326    ~ resource "test_instance" "example" {
  4327        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4328          id    = "i-02ae66f368e8518a9"
  4329          # (1 unchanged attribute hidden)
  4330  
  4331          # (2 unchanged blocks hidden)
  4332      }`,
  4333  		},
  4334  		"in-place update - multiple blocks first changed": {
  4335  			Action: plans.Update,
  4336  			Mode:   addrs.ManagedResourceMode,
  4337  			Before: cty.ObjectVal(map[string]cty.Value{
  4338  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4339  				"ami": cty.StringVal("ami-BEFORE"),
  4340  				"disks": cty.MapVal(map[string]cty.Value{
  4341  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4342  						"mount_point": cty.StringVal("/var/diska"),
  4343  						"size":        cty.StringVal("50GB"),
  4344  					}),
  4345  				}),
  4346  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4347  					"a": cty.ObjectVal(map[string]cty.Value{
  4348  						"volume_type": cty.StringVal("gp2"),
  4349  					}),
  4350  					"b": cty.ObjectVal(map[string]cty.Value{
  4351  						"volume_type": cty.StringVal("gp2"),
  4352  					}),
  4353  				}),
  4354  			}),
  4355  			After: cty.ObjectVal(map[string]cty.Value{
  4356  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4357  				"ami": cty.StringVal("ami-AFTER"),
  4358  				"disks": cty.MapVal(map[string]cty.Value{
  4359  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4360  						"mount_point": cty.StringVal("/var/diska"),
  4361  						"size":        cty.StringVal("50GB"),
  4362  					}),
  4363  				}),
  4364  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4365  					"a": cty.ObjectVal(map[string]cty.Value{
  4366  						"volume_type": cty.StringVal("gp2"),
  4367  					}),
  4368  					"b": cty.ObjectVal(map[string]cty.Value{
  4369  						"volume_type": cty.StringVal("gp3"),
  4370  					}),
  4371  				}),
  4372  			}),
  4373  			RequiredReplace: cty.NewPathSet(),
  4374  			Schema:          testSchema(configschema.NestingMap),
  4375  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4376    ~ resource "test_instance" "example" {
  4377        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4378          id    = "i-02ae66f368e8518a9"
  4379          # (1 unchanged attribute hidden)
  4380  
  4381        ~ root_block_device "b" {
  4382            ~ volume_type = "gp2" -> "gp3"
  4383          }
  4384  
  4385          # (1 unchanged block hidden)
  4386      }`,
  4387  		},
  4388  		"in-place update - multiple blocks second changed": {
  4389  			Action: plans.Update,
  4390  			Mode:   addrs.ManagedResourceMode,
  4391  			Before: cty.ObjectVal(map[string]cty.Value{
  4392  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4393  				"ami": cty.StringVal("ami-BEFORE"),
  4394  				"disks": cty.MapVal(map[string]cty.Value{
  4395  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4396  						"mount_point": cty.StringVal("/var/diska"),
  4397  						"size":        cty.StringVal("50GB"),
  4398  					}),
  4399  				}),
  4400  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4401  					"a": cty.ObjectVal(map[string]cty.Value{
  4402  						"volume_type": cty.StringVal("gp2"),
  4403  					}),
  4404  					"b": cty.ObjectVal(map[string]cty.Value{
  4405  						"volume_type": cty.StringVal("gp2"),
  4406  					}),
  4407  				}),
  4408  			}),
  4409  			After: cty.ObjectVal(map[string]cty.Value{
  4410  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4411  				"ami": cty.StringVal("ami-AFTER"),
  4412  				"disks": cty.MapVal(map[string]cty.Value{
  4413  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4414  						"mount_point": cty.StringVal("/var/diska"),
  4415  						"size":        cty.StringVal("50GB"),
  4416  					}),
  4417  				}),
  4418  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4419  					"a": cty.ObjectVal(map[string]cty.Value{
  4420  						"volume_type": cty.StringVal("gp3"),
  4421  					}),
  4422  					"b": cty.ObjectVal(map[string]cty.Value{
  4423  						"volume_type": cty.StringVal("gp2"),
  4424  					}),
  4425  				}),
  4426  			}),
  4427  			RequiredReplace: cty.NewPathSet(),
  4428  			Schema:          testSchema(configschema.NestingMap),
  4429  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4430    ~ resource "test_instance" "example" {
  4431        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4432          id    = "i-02ae66f368e8518a9"
  4433          # (1 unchanged attribute hidden)
  4434  
  4435        ~ root_block_device "a" {
  4436            ~ volume_type = "gp2" -> "gp3"
  4437          }
  4438  
  4439          # (1 unchanged block hidden)
  4440      }`,
  4441  		},
  4442  		"in-place update - multiple blocks changed": {
  4443  			Action: plans.Update,
  4444  			Mode:   addrs.ManagedResourceMode,
  4445  			Before: cty.ObjectVal(map[string]cty.Value{
  4446  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4447  				"ami": cty.StringVal("ami-BEFORE"),
  4448  				"disks": cty.MapVal(map[string]cty.Value{
  4449  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4450  						"mount_point": cty.StringVal("/var/diska"),
  4451  						"size":        cty.StringVal("50GB"),
  4452  					}),
  4453  				}),
  4454  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4455  					"a": cty.ObjectVal(map[string]cty.Value{
  4456  						"volume_type": cty.StringVal("gp2"),
  4457  					}),
  4458  					"b": cty.ObjectVal(map[string]cty.Value{
  4459  						"volume_type": cty.StringVal("gp2"),
  4460  					}),
  4461  				}),
  4462  			}),
  4463  			After: cty.ObjectVal(map[string]cty.Value{
  4464  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4465  				"ami": cty.StringVal("ami-AFTER"),
  4466  				"disks": cty.MapVal(map[string]cty.Value{
  4467  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4468  						"mount_point": cty.StringVal("/var/diska"),
  4469  						"size":        cty.StringVal("50GB"),
  4470  					}),
  4471  				}),
  4472  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4473  					"a": cty.ObjectVal(map[string]cty.Value{
  4474  						"volume_type": cty.StringVal("gp3"),
  4475  					}),
  4476  					"b": cty.ObjectVal(map[string]cty.Value{
  4477  						"volume_type": cty.StringVal("gp3"),
  4478  					}),
  4479  				}),
  4480  			}),
  4481  			RequiredReplace: cty.NewPathSet(),
  4482  			Schema:          testSchema(configschema.NestingMap),
  4483  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4484    ~ resource "test_instance" "example" {
  4485        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4486          id    = "i-02ae66f368e8518a9"
  4487          # (1 unchanged attribute hidden)
  4488  
  4489        ~ root_block_device "a" {
  4490            ~ volume_type = "gp2" -> "gp3"
  4491          }
  4492        ~ root_block_device "b" {
  4493            ~ volume_type = "gp2" -> "gp3"
  4494          }
  4495      }`,
  4496  		},
  4497  		"in-place update - multiple different unchanged blocks": {
  4498  			Action: plans.Update,
  4499  			Mode:   addrs.ManagedResourceMode,
  4500  			Before: cty.ObjectVal(map[string]cty.Value{
  4501  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4502  				"ami": cty.StringVal("ami-BEFORE"),
  4503  				"disks": cty.MapVal(map[string]cty.Value{
  4504  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4505  						"mount_point": cty.StringVal("/var/diska"),
  4506  						"size":        cty.StringVal("50GB"),
  4507  					}),
  4508  				}),
  4509  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4510  					"a": cty.ObjectVal(map[string]cty.Value{
  4511  						"volume_type": cty.StringVal("gp2"),
  4512  					}),
  4513  				}),
  4514  				"leaf_block_device": cty.MapVal(map[string]cty.Value{
  4515  					"b": cty.ObjectVal(map[string]cty.Value{
  4516  						"volume_type": cty.StringVal("gp2"),
  4517  					}),
  4518  				}),
  4519  			}),
  4520  			After: cty.ObjectVal(map[string]cty.Value{
  4521  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4522  				"ami": cty.StringVal("ami-AFTER"),
  4523  				"disks": cty.MapVal(map[string]cty.Value{
  4524  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4525  						"mount_point": cty.StringVal("/var/diska"),
  4526  						"size":        cty.StringVal("50GB"),
  4527  					}),
  4528  				}),
  4529  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4530  					"a": cty.ObjectVal(map[string]cty.Value{
  4531  						"volume_type": cty.StringVal("gp2"),
  4532  					}),
  4533  				}),
  4534  				"leaf_block_device": cty.MapVal(map[string]cty.Value{
  4535  					"b": cty.ObjectVal(map[string]cty.Value{
  4536  						"volume_type": cty.StringVal("gp2"),
  4537  					}),
  4538  				}),
  4539  			}),
  4540  			RequiredReplace: cty.NewPathSet(),
  4541  			Schema:          testSchemaMultipleBlocks(configschema.NestingMap),
  4542  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4543    ~ resource "test_instance" "example" {
  4544        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4545          id    = "i-02ae66f368e8518a9"
  4546          # (1 unchanged attribute hidden)
  4547  
  4548          # (2 unchanged blocks hidden)
  4549      }`,
  4550  		},
  4551  		"in-place update - multiple different blocks first changed": {
  4552  			Action: plans.Update,
  4553  			Mode:   addrs.ManagedResourceMode,
  4554  			Before: cty.ObjectVal(map[string]cty.Value{
  4555  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4556  				"ami": cty.StringVal("ami-BEFORE"),
  4557  				"disks": cty.MapVal(map[string]cty.Value{
  4558  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4559  						"mount_point": cty.StringVal("/var/diska"),
  4560  						"size":        cty.StringVal("50GB"),
  4561  					}),
  4562  				}),
  4563  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4564  					"a": cty.ObjectVal(map[string]cty.Value{
  4565  						"volume_type": cty.StringVal("gp2"),
  4566  					}),
  4567  				}),
  4568  				"leaf_block_device": cty.MapVal(map[string]cty.Value{
  4569  					"b": cty.ObjectVal(map[string]cty.Value{
  4570  						"volume_type": cty.StringVal("gp2"),
  4571  					}),
  4572  				}),
  4573  			}),
  4574  			After: cty.ObjectVal(map[string]cty.Value{
  4575  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4576  				"ami": cty.StringVal("ami-AFTER"),
  4577  				"disks": cty.MapVal(map[string]cty.Value{
  4578  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4579  						"mount_point": cty.StringVal("/var/diska"),
  4580  						"size":        cty.StringVal("50GB"),
  4581  					}),
  4582  				}),
  4583  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4584  					"a": cty.ObjectVal(map[string]cty.Value{
  4585  						"volume_type": cty.StringVal("gp2"),
  4586  					}),
  4587  				}),
  4588  				"leaf_block_device": cty.MapVal(map[string]cty.Value{
  4589  					"b": cty.ObjectVal(map[string]cty.Value{
  4590  						"volume_type": cty.StringVal("gp3"),
  4591  					}),
  4592  				}),
  4593  			}),
  4594  			RequiredReplace: cty.NewPathSet(),
  4595  			Schema:          testSchemaMultipleBlocks(configschema.NestingMap),
  4596  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4597    ~ resource "test_instance" "example" {
  4598        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4599          id    = "i-02ae66f368e8518a9"
  4600          # (1 unchanged attribute hidden)
  4601  
  4602        ~ leaf_block_device "b" {
  4603            ~ volume_type = "gp2" -> "gp3"
  4604          }
  4605  
  4606          # (1 unchanged block hidden)
  4607      }`,
  4608  		},
  4609  		"in-place update - multiple different blocks second changed": {
  4610  			Action: plans.Update,
  4611  			Mode:   addrs.ManagedResourceMode,
  4612  			Before: cty.ObjectVal(map[string]cty.Value{
  4613  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4614  				"ami": cty.StringVal("ami-BEFORE"),
  4615  				"disks": cty.MapVal(map[string]cty.Value{
  4616  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4617  						"mount_point": cty.StringVal("/var/diska"),
  4618  						"size":        cty.StringVal("50GB"),
  4619  					}),
  4620  				}),
  4621  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4622  					"a": cty.ObjectVal(map[string]cty.Value{
  4623  						"volume_type": cty.StringVal("gp2"),
  4624  					}),
  4625  				}),
  4626  				"leaf_block_device": cty.MapVal(map[string]cty.Value{
  4627  					"b": cty.ObjectVal(map[string]cty.Value{
  4628  						"volume_type": cty.StringVal("gp2"),
  4629  					}),
  4630  				}),
  4631  			}),
  4632  			After: cty.ObjectVal(map[string]cty.Value{
  4633  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4634  				"ami": cty.StringVal("ami-AFTER"),
  4635  				"disks": cty.MapVal(map[string]cty.Value{
  4636  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4637  						"mount_point": cty.StringVal("/var/diska"),
  4638  						"size":        cty.StringVal("50GB"),
  4639  					}),
  4640  				}),
  4641  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4642  					"a": cty.ObjectVal(map[string]cty.Value{
  4643  						"volume_type": cty.StringVal("gp3"),
  4644  					}),
  4645  				}),
  4646  				"leaf_block_device": cty.MapVal(map[string]cty.Value{
  4647  					"b": cty.ObjectVal(map[string]cty.Value{
  4648  						"volume_type": cty.StringVal("gp2"),
  4649  					}),
  4650  				}),
  4651  			}),
  4652  			RequiredReplace: cty.NewPathSet(),
  4653  			Schema:          testSchemaMultipleBlocks(configschema.NestingMap),
  4654  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4655    ~ resource "test_instance" "example" {
  4656        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4657          id    = "i-02ae66f368e8518a9"
  4658          # (1 unchanged attribute hidden)
  4659  
  4660        ~ root_block_device "a" {
  4661            ~ volume_type = "gp2" -> "gp3"
  4662          }
  4663  
  4664          # (1 unchanged block hidden)
  4665      }`,
  4666  		},
  4667  		"in-place update - multiple different blocks changed": {
  4668  			Action: plans.Update,
  4669  			Mode:   addrs.ManagedResourceMode,
  4670  			Before: cty.ObjectVal(map[string]cty.Value{
  4671  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4672  				"ami": cty.StringVal("ami-BEFORE"),
  4673  				"disks": cty.MapVal(map[string]cty.Value{
  4674  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4675  						"mount_point": cty.StringVal("/var/diska"),
  4676  						"size":        cty.StringVal("50GB"),
  4677  					}),
  4678  				}),
  4679  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4680  					"a": cty.ObjectVal(map[string]cty.Value{
  4681  						"volume_type": cty.StringVal("gp2"),
  4682  					}),
  4683  				}),
  4684  				"leaf_block_device": cty.MapVal(map[string]cty.Value{
  4685  					"b": cty.ObjectVal(map[string]cty.Value{
  4686  						"volume_type": cty.StringVal("gp2"),
  4687  					}),
  4688  				}),
  4689  			}),
  4690  			After: cty.ObjectVal(map[string]cty.Value{
  4691  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4692  				"ami": cty.StringVal("ami-AFTER"),
  4693  				"disks": cty.MapVal(map[string]cty.Value{
  4694  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4695  						"mount_point": cty.StringVal("/var/diska"),
  4696  						"size":        cty.StringVal("50GB"),
  4697  					}),
  4698  				}),
  4699  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4700  					"a": cty.ObjectVal(map[string]cty.Value{
  4701  						"volume_type": cty.StringVal("gp3"),
  4702  					}),
  4703  				}),
  4704  				"leaf_block_device": cty.MapVal(map[string]cty.Value{
  4705  					"b": cty.ObjectVal(map[string]cty.Value{
  4706  						"volume_type": cty.StringVal("gp3"),
  4707  					}),
  4708  				}),
  4709  			}),
  4710  			RequiredReplace: cty.NewPathSet(),
  4711  			Schema:          testSchemaMultipleBlocks(configschema.NestingMap),
  4712  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4713    ~ resource "test_instance" "example" {
  4714        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4715          id    = "i-02ae66f368e8518a9"
  4716          # (1 unchanged attribute hidden)
  4717  
  4718        ~ leaf_block_device "b" {
  4719            ~ volume_type = "gp2" -> "gp3"
  4720          }
  4721  
  4722        ~ root_block_device "a" {
  4723            ~ volume_type = "gp2" -> "gp3"
  4724          }
  4725      }`,
  4726  		},
  4727  		"in-place update - mixed blocks unchanged": {
  4728  			Action: plans.Update,
  4729  			Mode:   addrs.ManagedResourceMode,
  4730  			Before: cty.ObjectVal(map[string]cty.Value{
  4731  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4732  				"ami": cty.StringVal("ami-BEFORE"),
  4733  				"disks": cty.MapVal(map[string]cty.Value{
  4734  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4735  						"mount_point": cty.StringVal("/var/diska"),
  4736  						"size":        cty.StringVal("50GB"),
  4737  					}),
  4738  				}),
  4739  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4740  					"a": cty.ObjectVal(map[string]cty.Value{
  4741  						"volume_type": cty.StringVal("gp2"),
  4742  					}),
  4743  					"b": cty.ObjectVal(map[string]cty.Value{
  4744  						"volume_type": cty.StringVal("gp2"),
  4745  					}),
  4746  				}),
  4747  				"leaf_block_device": cty.MapVal(map[string]cty.Value{
  4748  					"a": cty.ObjectVal(map[string]cty.Value{
  4749  						"volume_type": cty.StringVal("gp2"),
  4750  					}),
  4751  					"b": cty.ObjectVal(map[string]cty.Value{
  4752  						"volume_type": cty.StringVal("gp2"),
  4753  					}),
  4754  				}),
  4755  			}),
  4756  			After: cty.ObjectVal(map[string]cty.Value{
  4757  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4758  				"ami": cty.StringVal("ami-AFTER"),
  4759  				"disks": cty.MapVal(map[string]cty.Value{
  4760  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4761  						"mount_point": cty.StringVal("/var/diska"),
  4762  						"size":        cty.StringVal("50GB"),
  4763  					}),
  4764  				}),
  4765  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4766  					"a": cty.ObjectVal(map[string]cty.Value{
  4767  						"volume_type": cty.StringVal("gp2"),
  4768  					}),
  4769  					"b": cty.ObjectVal(map[string]cty.Value{
  4770  						"volume_type": cty.StringVal("gp2"),
  4771  					}),
  4772  				}),
  4773  				"leaf_block_device": cty.MapVal(map[string]cty.Value{
  4774  					"a": cty.ObjectVal(map[string]cty.Value{
  4775  						"volume_type": cty.StringVal("gp2"),
  4776  					}),
  4777  					"b": cty.ObjectVal(map[string]cty.Value{
  4778  						"volume_type": cty.StringVal("gp2"),
  4779  					}),
  4780  				}),
  4781  			}),
  4782  			RequiredReplace: cty.NewPathSet(),
  4783  			Schema:          testSchemaMultipleBlocks(configschema.NestingMap),
  4784  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4785    ~ resource "test_instance" "example" {
  4786        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4787          id    = "i-02ae66f368e8518a9"
  4788          # (1 unchanged attribute hidden)
  4789  
  4790          # (4 unchanged blocks hidden)
  4791      }`,
  4792  		},
  4793  		"in-place update - mixed blocks changed": {
  4794  			Action: plans.Update,
  4795  			Mode:   addrs.ManagedResourceMode,
  4796  			Before: cty.ObjectVal(map[string]cty.Value{
  4797  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4798  				"ami": cty.StringVal("ami-BEFORE"),
  4799  				"disks": cty.MapVal(map[string]cty.Value{
  4800  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4801  						"mount_point": cty.StringVal("/var/diska"),
  4802  						"size":        cty.StringVal("50GB"),
  4803  					}),
  4804  				}),
  4805  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4806  					"a": cty.ObjectVal(map[string]cty.Value{
  4807  						"volume_type": cty.StringVal("gp2"),
  4808  					}),
  4809  					"b": cty.ObjectVal(map[string]cty.Value{
  4810  						"volume_type": cty.StringVal("gp2"),
  4811  					}),
  4812  				}),
  4813  				"leaf_block_device": cty.MapVal(map[string]cty.Value{
  4814  					"a": cty.ObjectVal(map[string]cty.Value{
  4815  						"volume_type": cty.StringVal("gp2"),
  4816  					}),
  4817  					"b": cty.ObjectVal(map[string]cty.Value{
  4818  						"volume_type": cty.StringVal("gp2"),
  4819  					}),
  4820  				}),
  4821  			}),
  4822  			After: cty.ObjectVal(map[string]cty.Value{
  4823  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4824  				"ami": cty.StringVal("ami-AFTER"),
  4825  				"disks": cty.MapVal(map[string]cty.Value{
  4826  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  4827  						"mount_point": cty.StringVal("/var/diska"),
  4828  						"size":        cty.StringVal("50GB"),
  4829  					}),
  4830  				}),
  4831  				"root_block_device": cty.MapVal(map[string]cty.Value{
  4832  					"a": cty.ObjectVal(map[string]cty.Value{
  4833  						"volume_type": cty.StringVal("gp2"),
  4834  					}),
  4835  					"b": cty.ObjectVal(map[string]cty.Value{
  4836  						"volume_type": cty.StringVal("gp3"),
  4837  					}),
  4838  				}),
  4839  				"leaf_block_device": cty.MapVal(map[string]cty.Value{
  4840  					"a": cty.ObjectVal(map[string]cty.Value{
  4841  						"volume_type": cty.StringVal("gp2"),
  4842  					}),
  4843  					"b": cty.ObjectVal(map[string]cty.Value{
  4844  						"volume_type": cty.StringVal("gp3"),
  4845  					}),
  4846  				}),
  4847  			}),
  4848  			RequiredReplace: cty.NewPathSet(),
  4849  			Schema:          testSchemaMultipleBlocks(configschema.NestingMap),
  4850  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4851    ~ resource "test_instance" "example" {
  4852        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  4853          id    = "i-02ae66f368e8518a9"
  4854          # (1 unchanged attribute hidden)
  4855  
  4856        ~ leaf_block_device "b" {
  4857            ~ volume_type = "gp2" -> "gp3"
  4858          }
  4859  
  4860        ~ root_block_device "b" {
  4861            ~ volume_type = "gp2" -> "gp3"
  4862          }
  4863  
  4864          # (2 unchanged blocks hidden)
  4865      }`,
  4866  		},
  4867  	}
  4868  	runTestCases(t, testCases)
  4869  }
  4870  
  4871  func TestResourceChange_nestedSingle(t *testing.T) {
  4872  	testCases := map[string]testCase{
  4873  		"in-place update - equal": {
  4874  			Action: plans.Update,
  4875  			Mode:   addrs.ManagedResourceMode,
  4876  			Before: cty.ObjectVal(map[string]cty.Value{
  4877  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4878  				"ami": cty.StringVal("ami-BEFORE"),
  4879  				"root_block_device": cty.ObjectVal(map[string]cty.Value{
  4880  					"volume_type": cty.StringVal("gp2"),
  4881  				}),
  4882  				"disk": cty.ObjectVal(map[string]cty.Value{
  4883  					"mount_point": cty.StringVal("/var/diska"),
  4884  					"size":        cty.StringVal("50GB"),
  4885  				}),
  4886  			}),
  4887  			After: cty.ObjectVal(map[string]cty.Value{
  4888  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4889  				"ami": cty.StringVal("ami-AFTER"),
  4890  				"root_block_device": cty.ObjectVal(map[string]cty.Value{
  4891  					"volume_type": cty.StringVal("gp2"),
  4892  				}),
  4893  				"disk": cty.ObjectVal(map[string]cty.Value{
  4894  					"mount_point": cty.StringVal("/var/diska"),
  4895  					"size":        cty.StringVal("50GB"),
  4896  				}),
  4897  			}),
  4898  			RequiredReplace: cty.NewPathSet(),
  4899  			Schema:          testSchema(configschema.NestingSingle),
  4900  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4901    ~ resource "test_instance" "example" {
  4902        ~ ami  = "ami-BEFORE" -> "ami-AFTER"
  4903          id   = "i-02ae66f368e8518a9"
  4904          # (1 unchanged attribute hidden)
  4905  
  4906          # (1 unchanged block hidden)
  4907      }`,
  4908  		},
  4909  		"in-place update - creation": {
  4910  			Action: plans.Update,
  4911  			Mode:   addrs.ManagedResourceMode,
  4912  			Before: cty.ObjectVal(map[string]cty.Value{
  4913  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4914  				"ami": cty.StringVal("ami-BEFORE"),
  4915  				"root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{
  4916  					"volume_type": cty.String,
  4917  				})),
  4918  				"disk": cty.NullVal(cty.Object(map[string]cty.Type{
  4919  					"mount_point": cty.String,
  4920  					"size":        cty.String,
  4921  				})),
  4922  			}),
  4923  			After: cty.ObjectVal(map[string]cty.Value{
  4924  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4925  				"ami": cty.StringVal("ami-AFTER"),
  4926  				"disk": cty.ObjectVal(map[string]cty.Value{
  4927  					"mount_point": cty.StringVal("/var/diska"),
  4928  					"size":        cty.StringVal("50GB"),
  4929  				}),
  4930  				"root_block_device": cty.ObjectVal(map[string]cty.Value{
  4931  					"volume_type": cty.NullVal(cty.String),
  4932  				}),
  4933  			}),
  4934  			RequiredReplace: cty.NewPathSet(),
  4935  			Schema:          testSchema(configschema.NestingSingle),
  4936  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4937    ~ resource "test_instance" "example" {
  4938        ~ ami  = "ami-BEFORE" -> "ami-AFTER"
  4939        + disk = {
  4940            + mount_point = "/var/diska"
  4941            + size        = "50GB"
  4942          }
  4943          id   = "i-02ae66f368e8518a9"
  4944  
  4945        + root_block_device {}
  4946      }`,
  4947  		},
  4948  		"force-new update (inside blocks)": {
  4949  			Action:       plans.DeleteThenCreate,
  4950  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  4951  			Mode:         addrs.ManagedResourceMode,
  4952  			Before: cty.ObjectVal(map[string]cty.Value{
  4953  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4954  				"ami": cty.StringVal("ami-BEFORE"),
  4955  				"disk": cty.ObjectVal(map[string]cty.Value{
  4956  					"mount_point": cty.StringVal("/var/diska"),
  4957  					"size":        cty.StringVal("50GB"),
  4958  				}),
  4959  				"root_block_device": cty.ObjectVal(map[string]cty.Value{
  4960  					"volume_type": cty.StringVal("gp2"),
  4961  				}),
  4962  			}),
  4963  			After: cty.ObjectVal(map[string]cty.Value{
  4964  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4965  				"ami": cty.StringVal("ami-AFTER"),
  4966  				"disk": cty.ObjectVal(map[string]cty.Value{
  4967  					"mount_point": cty.StringVal("/var/diskb"),
  4968  					"size":        cty.StringVal("50GB"),
  4969  				}),
  4970  				"root_block_device": cty.ObjectVal(map[string]cty.Value{
  4971  					"volume_type": cty.StringVal("different"),
  4972  				}),
  4973  			}),
  4974  			RequiredReplace: cty.NewPathSet(
  4975  				cty.Path{
  4976  					cty.GetAttrStep{Name: "root_block_device"},
  4977  					cty.GetAttrStep{Name: "volume_type"},
  4978  				},
  4979  				cty.Path{
  4980  					cty.GetAttrStep{Name: "disk"},
  4981  					cty.GetAttrStep{Name: "mount_point"},
  4982  				},
  4983  			),
  4984  			Schema: testSchema(configschema.NestingSingle),
  4985  			ExpectedOutput: `  # test_instance.example must be replaced
  4986  -/+ resource "test_instance" "example" {
  4987        ~ ami  = "ami-BEFORE" -> "ami-AFTER"
  4988        ~ disk = {
  4989            ~ mount_point = "/var/diska" -> "/var/diskb" # forces replacement
  4990              # (1 unchanged attribute hidden)
  4991          }
  4992          id   = "i-02ae66f368e8518a9"
  4993  
  4994        ~ root_block_device {
  4995            ~ volume_type = "gp2" -> "different" # forces replacement
  4996          }
  4997      }`,
  4998  		},
  4999  		"force-new update (whole block)": {
  5000  			Action:       plans.DeleteThenCreate,
  5001  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  5002  			Mode:         addrs.ManagedResourceMode,
  5003  			Before: cty.ObjectVal(map[string]cty.Value{
  5004  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5005  				"ami": cty.StringVal("ami-BEFORE"),
  5006  				"disk": cty.ObjectVal(map[string]cty.Value{
  5007  					"mount_point": cty.StringVal("/var/diska"),
  5008  					"size":        cty.StringVal("50GB"),
  5009  				}),
  5010  				"root_block_device": cty.ObjectVal(map[string]cty.Value{
  5011  					"volume_type": cty.StringVal("gp2"),
  5012  				}),
  5013  			}),
  5014  			After: cty.ObjectVal(map[string]cty.Value{
  5015  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5016  				"ami": cty.StringVal("ami-AFTER"),
  5017  				"disk": cty.ObjectVal(map[string]cty.Value{
  5018  					"mount_point": cty.StringVal("/var/diskb"),
  5019  					"size":        cty.StringVal("50GB"),
  5020  				}),
  5021  				"root_block_device": cty.ObjectVal(map[string]cty.Value{
  5022  					"volume_type": cty.StringVal("different"),
  5023  				}),
  5024  			}),
  5025  			RequiredReplace: cty.NewPathSet(
  5026  				cty.Path{cty.GetAttrStep{Name: "root_block_device"}},
  5027  				cty.Path{cty.GetAttrStep{Name: "disk"}},
  5028  			),
  5029  			Schema: testSchema(configschema.NestingSingle),
  5030  			ExpectedOutput: `  # test_instance.example must be replaced
  5031  -/+ resource "test_instance" "example" {
  5032        ~ ami  = "ami-BEFORE" -> "ami-AFTER"
  5033        ~ disk = { # forces replacement
  5034            ~ mount_point = "/var/diska" -> "/var/diskb"
  5035              # (1 unchanged attribute hidden)
  5036          }
  5037          id   = "i-02ae66f368e8518a9"
  5038  
  5039        ~ root_block_device { # forces replacement
  5040            ~ volume_type = "gp2" -> "different"
  5041          }
  5042      }`,
  5043  		},
  5044  		"in-place update - deletion": {
  5045  			Action: plans.Update,
  5046  			Mode:   addrs.ManagedResourceMode,
  5047  			Before: cty.ObjectVal(map[string]cty.Value{
  5048  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5049  				"ami": cty.StringVal("ami-BEFORE"),
  5050  				"disk": cty.ObjectVal(map[string]cty.Value{
  5051  					"mount_point": cty.StringVal("/var/diska"),
  5052  					"size":        cty.StringVal("50GB"),
  5053  				}),
  5054  				"root_block_device": cty.ObjectVal(map[string]cty.Value{
  5055  					"volume_type": cty.StringVal("gp2"),
  5056  				}),
  5057  			}),
  5058  			After: cty.ObjectVal(map[string]cty.Value{
  5059  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5060  				"ami": cty.StringVal("ami-AFTER"),
  5061  				"root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{
  5062  					"volume_type": cty.String,
  5063  				})),
  5064  				"disk": cty.NullVal(cty.Object(map[string]cty.Type{
  5065  					"mount_point": cty.String,
  5066  					"size":        cty.String,
  5067  				})),
  5068  			}),
  5069  			RequiredReplace: cty.NewPathSet(),
  5070  			Schema:          testSchema(configschema.NestingSingle),
  5071  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5072    ~ resource "test_instance" "example" {
  5073        ~ ami  = "ami-BEFORE" -> "ami-AFTER"
  5074        - disk = {
  5075            - mount_point = "/var/diska" -> null
  5076            - size        = "50GB" -> null
  5077          } -> null
  5078          id   = "i-02ae66f368e8518a9"
  5079  
  5080        - root_block_device {
  5081            - volume_type = "gp2" -> null
  5082          }
  5083      }`,
  5084  		},
  5085  		"with dynamically-typed attribute": {
  5086  			Action: plans.Update,
  5087  			Mode:   addrs.ManagedResourceMode,
  5088  			Before: cty.ObjectVal(map[string]cty.Value{
  5089  				"block": cty.NullVal(cty.Object(map[string]cty.Type{
  5090  					"attr": cty.String,
  5091  				})),
  5092  			}),
  5093  			After: cty.ObjectVal(map[string]cty.Value{
  5094  				"block": cty.ObjectVal(map[string]cty.Value{
  5095  					"attr": cty.StringVal("foo"),
  5096  				}),
  5097  			}),
  5098  			RequiredReplace: cty.NewPathSet(),
  5099  			Schema: &configschema.Block{
  5100  				BlockTypes: map[string]*configschema.NestedBlock{
  5101  					"block": {
  5102  						Block: configschema.Block{
  5103  							Attributes: map[string]*configschema.Attribute{
  5104  								"attr": {Type: cty.DynamicPseudoType, Optional: true},
  5105  							},
  5106  						},
  5107  						Nesting: configschema.NestingSingle,
  5108  					},
  5109  				},
  5110  			},
  5111  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5112    ~ resource "test_instance" "example" {
  5113        + block {
  5114            + attr = "foo"
  5115          }
  5116      }`,
  5117  		},
  5118  		"in-place update - unknown": {
  5119  			Action: plans.Update,
  5120  			Mode:   addrs.ManagedResourceMode,
  5121  			Before: cty.ObjectVal(map[string]cty.Value{
  5122  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5123  				"ami": cty.StringVal("ami-BEFORE"),
  5124  				"disk": cty.ObjectVal(map[string]cty.Value{
  5125  					"mount_point": cty.StringVal("/var/diska"),
  5126  					"size":        cty.StringVal("50GB"),
  5127  				}),
  5128  				"root_block_device": cty.ObjectVal(map[string]cty.Value{
  5129  					"volume_type": cty.StringVal("gp2"),
  5130  					"new_field":   cty.StringVal("new_value"),
  5131  				}),
  5132  			}),
  5133  			After: cty.ObjectVal(map[string]cty.Value{
  5134  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5135  				"ami": cty.StringVal("ami-AFTER"),
  5136  				"disk": cty.UnknownVal(cty.Object(map[string]cty.Type{
  5137  					"mount_point": cty.String,
  5138  					"size":        cty.String,
  5139  				})),
  5140  				"root_block_device": cty.ObjectVal(map[string]cty.Value{
  5141  					"volume_type": cty.StringVal("gp2"),
  5142  					"new_field":   cty.StringVal("new_value"),
  5143  				}),
  5144  			}),
  5145  			RequiredReplace: cty.NewPathSet(),
  5146  			Schema:          testSchemaPlus(configschema.NestingSingle),
  5147  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5148    ~ resource "test_instance" "example" {
  5149        ~ ami  = "ami-BEFORE" -> "ami-AFTER"
  5150        ~ disk = {
  5151            ~ mount_point = "/var/diska" -> (known after apply)
  5152            ~ size        = "50GB" -> (known after apply)
  5153          } -> (known after apply)
  5154          id   = "i-02ae66f368e8518a9"
  5155  
  5156          # (1 unchanged block hidden)
  5157      }`,
  5158  		},
  5159  		"in-place update - modification": {
  5160  			Action: plans.Update,
  5161  			Mode:   addrs.ManagedResourceMode,
  5162  			Before: cty.ObjectVal(map[string]cty.Value{
  5163  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5164  				"ami": cty.StringVal("ami-BEFORE"),
  5165  				"disk": cty.ObjectVal(map[string]cty.Value{
  5166  					"mount_point": cty.StringVal("/var/diska"),
  5167  					"size":        cty.StringVal("50GB"),
  5168  				}),
  5169  				"root_block_device": cty.ObjectVal(map[string]cty.Value{
  5170  					"volume_type": cty.StringVal("gp2"),
  5171  					"new_field":   cty.StringVal("new_value"),
  5172  				}),
  5173  			}),
  5174  			After: cty.ObjectVal(map[string]cty.Value{
  5175  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5176  				"ami": cty.StringVal("ami-AFTER"),
  5177  				"disk": cty.ObjectVal(map[string]cty.Value{
  5178  					"mount_point": cty.StringVal("/var/diska"),
  5179  					"size":        cty.StringVal("25GB"),
  5180  				}),
  5181  				"root_block_device": cty.ObjectVal(map[string]cty.Value{
  5182  					"volume_type": cty.StringVal("gp2"),
  5183  					"new_field":   cty.StringVal("new_value"),
  5184  				}),
  5185  			}),
  5186  			RequiredReplace: cty.NewPathSet(),
  5187  			Schema:          testSchemaPlus(configschema.NestingSingle),
  5188  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5189    ~ resource "test_instance" "example" {
  5190        ~ ami  = "ami-BEFORE" -> "ami-AFTER"
  5191        ~ disk = {
  5192            ~ size        = "50GB" -> "25GB"
  5193              # (1 unchanged attribute hidden)
  5194          }
  5195          id   = "i-02ae66f368e8518a9"
  5196  
  5197          # (1 unchanged block hidden)
  5198      }`,
  5199  		},
  5200  	}
  5201  	runTestCases(t, testCases)
  5202  }
  5203  
  5204  func TestResourceChange_nestedMapSensitiveSchema(t *testing.T) {
  5205  	testCases := map[string]testCase{
  5206  		"creation from null": {
  5207  			Action: plans.Update,
  5208  			Mode:   addrs.ManagedResourceMode,
  5209  			Before: cty.ObjectVal(map[string]cty.Value{
  5210  				"id":  cty.NullVal(cty.String),
  5211  				"ami": cty.NullVal(cty.String),
  5212  				"disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
  5213  					"mount_point": cty.String,
  5214  					"size":        cty.String,
  5215  				}))),
  5216  			}),
  5217  			After: cty.ObjectVal(map[string]cty.Value{
  5218  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5219  				"ami": cty.StringVal("ami-AFTER"),
  5220  				"disks": cty.MapVal(map[string]cty.Value{
  5221  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  5222  						"mount_point": cty.StringVal("/var/diska"),
  5223  						"size":        cty.NullVal(cty.String),
  5224  					}),
  5225  				}),
  5226  			}),
  5227  			RequiredReplace: cty.NewPathSet(),
  5228  			Schema:          testSchemaSensitive(configschema.NestingMap),
  5229  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5230    ~ resource "test_instance" "example" {
  5231        + ami   = "ami-AFTER"
  5232        + disks = (sensitive value)
  5233        + id    = "i-02ae66f368e8518a9"
  5234      }`,
  5235  		},
  5236  		"in-place update": {
  5237  			Action: plans.Update,
  5238  			Mode:   addrs.ManagedResourceMode,
  5239  			Before: cty.ObjectVal(map[string]cty.Value{
  5240  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5241  				"ami": cty.StringVal("ami-BEFORE"),
  5242  				"disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  5243  					"mount_point": cty.String,
  5244  					"size":        cty.String,
  5245  				})),
  5246  			}),
  5247  			After: cty.ObjectVal(map[string]cty.Value{
  5248  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5249  				"ami": cty.StringVal("ami-AFTER"),
  5250  				"disks": cty.MapVal(map[string]cty.Value{
  5251  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  5252  						"mount_point": cty.StringVal("/var/diska"),
  5253  						"size":        cty.NullVal(cty.String),
  5254  					}),
  5255  				}),
  5256  			}),
  5257  			RequiredReplace: cty.NewPathSet(),
  5258  			Schema:          testSchemaSensitive(configschema.NestingMap),
  5259  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5260    ~ resource "test_instance" "example" {
  5261        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  5262        ~ disks = (sensitive value)
  5263          id    = "i-02ae66f368e8518a9"
  5264      }`,
  5265  		},
  5266  		"force-new update (whole block)": {
  5267  			Action:       plans.DeleteThenCreate,
  5268  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  5269  			Mode:         addrs.ManagedResourceMode,
  5270  			Before: cty.ObjectVal(map[string]cty.Value{
  5271  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5272  				"ami": cty.StringVal("ami-BEFORE"),
  5273  				"disks": cty.MapVal(map[string]cty.Value{
  5274  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  5275  						"mount_point": cty.StringVal("/var/diska"),
  5276  						"size":        cty.StringVal("50GB"),
  5277  					}),
  5278  				}),
  5279  			}),
  5280  			After: cty.ObjectVal(map[string]cty.Value{
  5281  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5282  				"ami": cty.StringVal("ami-AFTER"),
  5283  				"disks": cty.MapVal(map[string]cty.Value{
  5284  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  5285  						"mount_point": cty.StringVal("/var/diska"),
  5286  						"size":        cty.StringVal("100GB"),
  5287  					}),
  5288  				}),
  5289  			}),
  5290  			RequiredReplace: cty.NewPathSet(
  5291  				cty.Path{cty.GetAttrStep{Name: "disks"}},
  5292  			),
  5293  			Schema: testSchemaSensitive(configschema.NestingMap),
  5294  			ExpectedOutput: `  # test_instance.example must be replaced
  5295  -/+ resource "test_instance" "example" {
  5296        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  5297        ~ disks = (sensitive value) # forces replacement
  5298          id    = "i-02ae66f368e8518a9"
  5299      }`,
  5300  		},
  5301  		"in-place update - deletion": {
  5302  			Action: plans.Update,
  5303  			Mode:   addrs.ManagedResourceMode,
  5304  			Before: cty.ObjectVal(map[string]cty.Value{
  5305  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5306  				"ami": cty.StringVal("ami-BEFORE"),
  5307  				"disks": cty.MapVal(map[string]cty.Value{
  5308  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  5309  						"mount_point": cty.StringVal("/var/diska"),
  5310  						"size":        cty.StringVal("50GB"),
  5311  					}),
  5312  				}),
  5313  			}),
  5314  			After: cty.ObjectVal(map[string]cty.Value{
  5315  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5316  				"ami": cty.StringVal("ami-AFTER"),
  5317  				"disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
  5318  					"mount_point": cty.String,
  5319  					"size":        cty.String,
  5320  				}))),
  5321  			}),
  5322  			RequiredReplace: cty.NewPathSet(),
  5323  			Schema:          testSchemaSensitive(configschema.NestingMap),
  5324  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5325    ~ resource "test_instance" "example" {
  5326        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  5327        - disks = (sensitive value) -> null
  5328          id    = "i-02ae66f368e8518a9"
  5329      }`,
  5330  		},
  5331  		"in-place update - unknown": {
  5332  			Action: plans.Update,
  5333  			Mode:   addrs.ManagedResourceMode,
  5334  			Before: cty.ObjectVal(map[string]cty.Value{
  5335  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5336  				"ami": cty.StringVal("ami-BEFORE"),
  5337  				"disks": cty.MapVal(map[string]cty.Value{
  5338  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  5339  						"mount_point": cty.StringVal("/var/diska"),
  5340  						"size":        cty.StringVal("50GB"),
  5341  					}),
  5342  				}),
  5343  			}),
  5344  			After: cty.ObjectVal(map[string]cty.Value{
  5345  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5346  				"ami": cty.StringVal("ami-AFTER"),
  5347  				"disks": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{
  5348  					"mount_point": cty.String,
  5349  					"size":        cty.String,
  5350  				}))),
  5351  			}),
  5352  			RequiredReplace: cty.NewPathSet(),
  5353  			Schema:          testSchemaSensitive(configschema.NestingMap),
  5354  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5355    ~ resource "test_instance" "example" {
  5356        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  5357        ~ disks = (sensitive value)
  5358          id    = "i-02ae66f368e8518a9"
  5359      }`,
  5360  		},
  5361  	}
  5362  	runTestCases(t, testCases)
  5363  }
  5364  
  5365  func TestResourceChange_nestedListSensitiveSchema(t *testing.T) {
  5366  	testCases := map[string]testCase{
  5367  		"creation from null": {
  5368  			Action: plans.Update,
  5369  			Mode:   addrs.ManagedResourceMode,
  5370  			Before: cty.ObjectVal(map[string]cty.Value{
  5371  				"id":  cty.NullVal(cty.String),
  5372  				"ami": cty.NullVal(cty.String),
  5373  				"disks": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
  5374  					"mount_point": cty.String,
  5375  					"size":        cty.String,
  5376  				}))),
  5377  			}),
  5378  			After: cty.ObjectVal(map[string]cty.Value{
  5379  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5380  				"ami": cty.StringVal("ami-AFTER"),
  5381  				"disks": cty.ListVal([]cty.Value{
  5382  					cty.ObjectVal(map[string]cty.Value{
  5383  						"mount_point": cty.StringVal("/var/diska"),
  5384  						"size":        cty.NullVal(cty.String),
  5385  					}),
  5386  				}),
  5387  			}),
  5388  			RequiredReplace: cty.NewPathSet(),
  5389  			Schema:          testSchemaSensitive(configschema.NestingList),
  5390  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5391    ~ resource "test_instance" "example" {
  5392        + ami   = "ami-AFTER"
  5393        + disks = (sensitive value)
  5394        + id    = "i-02ae66f368e8518a9"
  5395      }`,
  5396  		},
  5397  		"in-place update": {
  5398  			Action: plans.Update,
  5399  			Mode:   addrs.ManagedResourceMode,
  5400  			Before: cty.ObjectVal(map[string]cty.Value{
  5401  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5402  				"ami": cty.StringVal("ami-BEFORE"),
  5403  				"disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  5404  					"mount_point": cty.String,
  5405  					"size":        cty.String,
  5406  				})),
  5407  			}),
  5408  			After: cty.ObjectVal(map[string]cty.Value{
  5409  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5410  				"ami": cty.StringVal("ami-AFTER"),
  5411  				"disks": cty.ListVal([]cty.Value{
  5412  					cty.ObjectVal(map[string]cty.Value{
  5413  						"mount_point": cty.StringVal("/var/diska"),
  5414  						"size":        cty.NullVal(cty.String),
  5415  					}),
  5416  				}),
  5417  			}),
  5418  			RequiredReplace: cty.NewPathSet(),
  5419  			Schema:          testSchemaSensitive(configschema.NestingList),
  5420  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5421    ~ resource "test_instance" "example" {
  5422        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  5423        ~ disks = (sensitive value)
  5424          id    = "i-02ae66f368e8518a9"
  5425      }`,
  5426  		},
  5427  		"force-new update (whole block)": {
  5428  			Action:       plans.DeleteThenCreate,
  5429  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  5430  			Mode:         addrs.ManagedResourceMode,
  5431  			Before: cty.ObjectVal(map[string]cty.Value{
  5432  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5433  				"ami": cty.StringVal("ami-BEFORE"),
  5434  				"disks": cty.ListVal([]cty.Value{
  5435  					cty.ObjectVal(map[string]cty.Value{
  5436  						"mount_point": cty.StringVal("/var/diska"),
  5437  						"size":        cty.StringVal("50GB"),
  5438  					}),
  5439  				}),
  5440  			}),
  5441  			After: cty.ObjectVal(map[string]cty.Value{
  5442  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5443  				"ami": cty.StringVal("ami-AFTER"),
  5444  				"disks": cty.ListVal([]cty.Value{
  5445  					cty.ObjectVal(map[string]cty.Value{
  5446  						"mount_point": cty.StringVal("/var/diska"),
  5447  						"size":        cty.StringVal("100GB"),
  5448  					}),
  5449  				}),
  5450  			}),
  5451  			RequiredReplace: cty.NewPathSet(
  5452  				cty.Path{cty.GetAttrStep{Name: "disks"}},
  5453  			),
  5454  			Schema: testSchemaSensitive(configschema.NestingList),
  5455  			ExpectedOutput: `  # test_instance.example must be replaced
  5456  -/+ resource "test_instance" "example" {
  5457        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  5458        ~ disks = (sensitive value) # forces replacement
  5459          id    = "i-02ae66f368e8518a9"
  5460      }`,
  5461  		},
  5462  		"in-place update - deletion": {
  5463  			Action: plans.Update,
  5464  			Mode:   addrs.ManagedResourceMode,
  5465  			Before: cty.ObjectVal(map[string]cty.Value{
  5466  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5467  				"ami": cty.StringVal("ami-BEFORE"),
  5468  				"disks": cty.ListVal([]cty.Value{
  5469  					cty.ObjectVal(map[string]cty.Value{
  5470  						"mount_point": cty.StringVal("/var/diska"),
  5471  						"size":        cty.StringVal("50GB"),
  5472  					}),
  5473  				}),
  5474  			}),
  5475  			After: cty.ObjectVal(map[string]cty.Value{
  5476  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5477  				"ami": cty.StringVal("ami-AFTER"),
  5478  				"disks": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
  5479  					"mount_point": cty.String,
  5480  					"size":        cty.String,
  5481  				}))),
  5482  			}),
  5483  			RequiredReplace: cty.NewPathSet(),
  5484  			Schema:          testSchemaSensitive(configschema.NestingList),
  5485  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5486    ~ resource "test_instance" "example" {
  5487        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  5488        - disks = (sensitive value) -> null
  5489          id    = "i-02ae66f368e8518a9"
  5490      }`,
  5491  		},
  5492  		"in-place update - unknown": {
  5493  			Action: plans.Update,
  5494  			Mode:   addrs.ManagedResourceMode,
  5495  			Before: cty.ObjectVal(map[string]cty.Value{
  5496  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5497  				"ami": cty.StringVal("ami-BEFORE"),
  5498  				"disks": cty.ListVal([]cty.Value{
  5499  					cty.ObjectVal(map[string]cty.Value{
  5500  						"mount_point": cty.StringVal("/var/diska"),
  5501  						"size":        cty.StringVal("50GB"),
  5502  					}),
  5503  				}),
  5504  			}),
  5505  			After: cty.ObjectVal(map[string]cty.Value{
  5506  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5507  				"ami": cty.StringVal("ami-AFTER"),
  5508  				"disks": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
  5509  					"mount_point": cty.String,
  5510  					"size":        cty.String,
  5511  				}))),
  5512  			}),
  5513  			RequiredReplace: cty.NewPathSet(),
  5514  			Schema:          testSchemaSensitive(configschema.NestingList),
  5515  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5516    ~ resource "test_instance" "example" {
  5517        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  5518        ~ disks = (sensitive value)
  5519          id    = "i-02ae66f368e8518a9"
  5520      }`,
  5521  		},
  5522  	}
  5523  	runTestCases(t, testCases)
  5524  }
  5525  
  5526  func TestResourceChange_nestedSetSensitiveSchema(t *testing.T) {
  5527  	testCases := map[string]testCase{
  5528  		"creation from null": {
  5529  			Action: plans.Update,
  5530  			Mode:   addrs.ManagedResourceMode,
  5531  			Before: cty.ObjectVal(map[string]cty.Value{
  5532  				"id":  cty.NullVal(cty.String),
  5533  				"ami": cty.NullVal(cty.String),
  5534  				"disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
  5535  					"mount_point": cty.String,
  5536  					"size":        cty.String,
  5537  				}))),
  5538  			}),
  5539  			After: cty.ObjectVal(map[string]cty.Value{
  5540  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5541  				"ami": cty.StringVal("ami-AFTER"),
  5542  				"disks": cty.SetVal([]cty.Value{
  5543  					cty.ObjectVal(map[string]cty.Value{
  5544  						"mount_point": cty.StringVal("/var/diska"),
  5545  						"size":        cty.NullVal(cty.String),
  5546  					}),
  5547  				}),
  5548  			}),
  5549  			RequiredReplace: cty.NewPathSet(),
  5550  			Schema:          testSchemaSensitive(configschema.NestingSet),
  5551  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5552    ~ resource "test_instance" "example" {
  5553        + ami   = "ami-AFTER"
  5554        + disks = (sensitive value)
  5555        + id    = "i-02ae66f368e8518a9"
  5556      }`,
  5557  		},
  5558  		"in-place update": {
  5559  			Action: plans.Update,
  5560  			Mode:   addrs.ManagedResourceMode,
  5561  			Before: cty.ObjectVal(map[string]cty.Value{
  5562  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5563  				"ami": cty.StringVal("ami-BEFORE"),
  5564  				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  5565  					"mount_point": cty.String,
  5566  					"size":        cty.String,
  5567  				})),
  5568  			}),
  5569  			After: cty.ObjectVal(map[string]cty.Value{
  5570  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5571  				"ami": cty.StringVal("ami-AFTER"),
  5572  				"disks": cty.SetVal([]cty.Value{
  5573  					cty.ObjectVal(map[string]cty.Value{
  5574  						"mount_point": cty.StringVal("/var/diska"),
  5575  						"size":        cty.NullVal(cty.String),
  5576  					}),
  5577  				}),
  5578  			}),
  5579  			RequiredReplace: cty.NewPathSet(),
  5580  			Schema:          testSchemaSensitive(configschema.NestingSet),
  5581  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5582    ~ resource "test_instance" "example" {
  5583        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  5584        ~ disks = (sensitive value)
  5585          id    = "i-02ae66f368e8518a9"
  5586      }`,
  5587  		},
  5588  		"force-new update (whole block)": {
  5589  			Action:       plans.DeleteThenCreate,
  5590  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  5591  			Mode:         addrs.ManagedResourceMode,
  5592  			Before: cty.ObjectVal(map[string]cty.Value{
  5593  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5594  				"ami": cty.StringVal("ami-BEFORE"),
  5595  				"disks": cty.SetVal([]cty.Value{
  5596  					cty.ObjectVal(map[string]cty.Value{
  5597  						"mount_point": cty.StringVal("/var/diska"),
  5598  						"size":        cty.StringVal("50GB"),
  5599  					}),
  5600  				}),
  5601  			}),
  5602  			After: cty.ObjectVal(map[string]cty.Value{
  5603  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5604  				"ami": cty.StringVal("ami-AFTER"),
  5605  				"disks": cty.SetVal([]cty.Value{
  5606  					cty.ObjectVal(map[string]cty.Value{
  5607  						"mount_point": cty.StringVal("/var/diska"),
  5608  						"size":        cty.StringVal("100GB"),
  5609  					}),
  5610  				}),
  5611  			}),
  5612  			RequiredReplace: cty.NewPathSet(
  5613  				cty.Path{cty.GetAttrStep{Name: "disks"}},
  5614  			),
  5615  			Schema: testSchemaSensitive(configschema.NestingSet),
  5616  			ExpectedOutput: `  # test_instance.example must be replaced
  5617  -/+ resource "test_instance" "example" {
  5618        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  5619        ~ disks = (sensitive value) # forces replacement
  5620          id    = "i-02ae66f368e8518a9"
  5621      }`,
  5622  		},
  5623  		"in-place update - deletion": {
  5624  			Action: plans.Update,
  5625  			Mode:   addrs.ManagedResourceMode,
  5626  			Before: cty.ObjectVal(map[string]cty.Value{
  5627  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5628  				"ami": cty.StringVal("ami-BEFORE"),
  5629  				"disks": cty.SetVal([]cty.Value{
  5630  					cty.ObjectVal(map[string]cty.Value{
  5631  						"mount_point": cty.StringVal("/var/diska"),
  5632  						"size":        cty.StringVal("50GB"),
  5633  					}),
  5634  				}),
  5635  			}),
  5636  			After: cty.ObjectVal(map[string]cty.Value{
  5637  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5638  				"ami": cty.StringVal("ami-AFTER"),
  5639  				"disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
  5640  					"mount_point": cty.String,
  5641  					"size":        cty.String,
  5642  				}))),
  5643  			}),
  5644  			RequiredReplace: cty.NewPathSet(),
  5645  			Schema:          testSchemaSensitive(configschema.NestingSet),
  5646  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5647    ~ resource "test_instance" "example" {
  5648        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  5649        - disks = (sensitive value) -> null
  5650          id    = "i-02ae66f368e8518a9"
  5651      }`,
  5652  		},
  5653  		"in-place update - unknown": {
  5654  			Action: plans.Update,
  5655  			Mode:   addrs.ManagedResourceMode,
  5656  			Before: cty.ObjectVal(map[string]cty.Value{
  5657  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5658  				"ami": cty.StringVal("ami-BEFORE"),
  5659  				"disks": cty.SetVal([]cty.Value{
  5660  					cty.ObjectVal(map[string]cty.Value{
  5661  						"mount_point": cty.StringVal("/var/diska"),
  5662  						"size":        cty.StringVal("50GB"),
  5663  					}),
  5664  				}),
  5665  			}),
  5666  			After: cty.ObjectVal(map[string]cty.Value{
  5667  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5668  				"ami": cty.StringVal("ami-AFTER"),
  5669  				"disks": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
  5670  					"mount_point": cty.String,
  5671  					"size":        cty.String,
  5672  				}))),
  5673  			}),
  5674  			RequiredReplace: cty.NewPathSet(),
  5675  			Schema:          testSchemaSensitive(configschema.NestingSet),
  5676  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5677    ~ resource "test_instance" "example" {
  5678        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  5679        ~ disks = (sensitive value)
  5680          id    = "i-02ae66f368e8518a9"
  5681      }`,
  5682  		},
  5683  	}
  5684  	runTestCases(t, testCases)
  5685  }
  5686  
  5687  func TestResourceChange_actionReason(t *testing.T) {
  5688  	emptySchema := &configschema.Block{}
  5689  	nullVal := cty.NullVal(cty.EmptyObject)
  5690  	emptyVal := cty.EmptyObjectVal
  5691  
  5692  	testCases := map[string]testCase{
  5693  		"delete for no particular reason": {
  5694  			Action:          plans.Delete,
  5695  			ActionReason:    plans.ResourceInstanceChangeNoReason,
  5696  			Mode:            addrs.ManagedResourceMode,
  5697  			Before:          emptyVal,
  5698  			After:           nullVal,
  5699  			Schema:          emptySchema,
  5700  			RequiredReplace: cty.NewPathSet(),
  5701  			ExpectedOutput: `  # test_instance.example will be destroyed
  5702    - resource "test_instance" "example" {}`,
  5703  		},
  5704  		"delete because of wrong repetition mode (NoKey)": {
  5705  			Action:          plans.Delete,
  5706  			ActionReason:    plans.ResourceInstanceDeleteBecauseWrongRepetition,
  5707  			Mode:            addrs.ManagedResourceMode,
  5708  			InstanceKey:     addrs.NoKey,
  5709  			Before:          emptyVal,
  5710  			After:           nullVal,
  5711  			Schema:          emptySchema,
  5712  			RequiredReplace: cty.NewPathSet(),
  5713  			ExpectedOutput: `  # test_instance.example will be destroyed
  5714    # (because resource uses count or for_each)
  5715    - resource "test_instance" "example" {}`,
  5716  		},
  5717  		"delete because of wrong repetition mode (IntKey)": {
  5718  			Action:          plans.Delete,
  5719  			ActionReason:    plans.ResourceInstanceDeleteBecauseWrongRepetition,
  5720  			Mode:            addrs.ManagedResourceMode,
  5721  			InstanceKey:     addrs.IntKey(1),
  5722  			Before:          emptyVal,
  5723  			After:           nullVal,
  5724  			Schema:          emptySchema,
  5725  			RequiredReplace: cty.NewPathSet(),
  5726  			ExpectedOutput: `  # test_instance.example[1] will be destroyed
  5727    # (because resource does not use count)
  5728    - resource "test_instance" "example" {}`,
  5729  		},
  5730  		"delete because of wrong repetition mode (StringKey)": {
  5731  			Action:          plans.Delete,
  5732  			ActionReason:    plans.ResourceInstanceDeleteBecauseWrongRepetition,
  5733  			Mode:            addrs.ManagedResourceMode,
  5734  			InstanceKey:     addrs.StringKey("a"),
  5735  			Before:          emptyVal,
  5736  			After:           nullVal,
  5737  			Schema:          emptySchema,
  5738  			RequiredReplace: cty.NewPathSet(),
  5739  			ExpectedOutput: `  # test_instance.example["a"] will be destroyed
  5740    # (because resource does not use for_each)
  5741    - resource "test_instance" "example" {}`,
  5742  		},
  5743  		"delete because no resource configuration": {
  5744  			Action:          plans.Delete,
  5745  			ActionReason:    plans.ResourceInstanceDeleteBecauseNoResourceConfig,
  5746  			ModuleInst:      addrs.RootModuleInstance.Child("foo", addrs.NoKey),
  5747  			Mode:            addrs.ManagedResourceMode,
  5748  			Before:          emptyVal,
  5749  			After:           nullVal,
  5750  			Schema:          emptySchema,
  5751  			RequiredReplace: cty.NewPathSet(),
  5752  			ExpectedOutput: `  # module.foo.test_instance.example will be destroyed
  5753    # (because test_instance.example is not in configuration)
  5754    - resource "test_instance" "example" {}`,
  5755  		},
  5756  		"delete because no module": {
  5757  			Action:          plans.Delete,
  5758  			ActionReason:    plans.ResourceInstanceDeleteBecauseNoModule,
  5759  			ModuleInst:      addrs.RootModuleInstance.Child("foo", addrs.IntKey(1)),
  5760  			Mode:            addrs.ManagedResourceMode,
  5761  			Before:          emptyVal,
  5762  			After:           nullVal,
  5763  			Schema:          emptySchema,
  5764  			RequiredReplace: cty.NewPathSet(),
  5765  			ExpectedOutput: `  # module.foo[1].test_instance.example will be destroyed
  5766    # (because module.foo[1] is not in configuration)
  5767    - resource "test_instance" "example" {}`,
  5768  		},
  5769  		"delete because out of range for count": {
  5770  			Action:          plans.Delete,
  5771  			ActionReason:    plans.ResourceInstanceDeleteBecauseCountIndex,
  5772  			Mode:            addrs.ManagedResourceMode,
  5773  			InstanceKey:     addrs.IntKey(1),
  5774  			Before:          emptyVal,
  5775  			After:           nullVal,
  5776  			Schema:          emptySchema,
  5777  			RequiredReplace: cty.NewPathSet(),
  5778  			ExpectedOutput: `  # test_instance.example[1] will be destroyed
  5779    # (because index [1] is out of range for count)
  5780    - resource "test_instance" "example" {}`,
  5781  		},
  5782  		"delete because out of range for for_each": {
  5783  			Action:          plans.Delete,
  5784  			ActionReason:    plans.ResourceInstanceDeleteBecauseEachKey,
  5785  			Mode:            addrs.ManagedResourceMode,
  5786  			InstanceKey:     addrs.StringKey("boop"),
  5787  			Before:          emptyVal,
  5788  			After:           nullVal,
  5789  			Schema:          emptySchema,
  5790  			RequiredReplace: cty.NewPathSet(),
  5791  			ExpectedOutput: `  # test_instance.example["boop"] will be destroyed
  5792    # (because key ["boop"] is not in for_each map)
  5793    - resource "test_instance" "example" {}`,
  5794  		},
  5795  		"replace for no particular reason (delete first)": {
  5796  			Action:          plans.DeleteThenCreate,
  5797  			ActionReason:    plans.ResourceInstanceChangeNoReason,
  5798  			Mode:            addrs.ManagedResourceMode,
  5799  			Before:          emptyVal,
  5800  			After:           nullVal,
  5801  			Schema:          emptySchema,
  5802  			RequiredReplace: cty.NewPathSet(),
  5803  			ExpectedOutput: `  # test_instance.example must be replaced
  5804  -/+ resource "test_instance" "example" {}`,
  5805  		},
  5806  		"replace for no particular reason (create first)": {
  5807  			Action:          plans.CreateThenDelete,
  5808  			ActionReason:    plans.ResourceInstanceChangeNoReason,
  5809  			Mode:            addrs.ManagedResourceMode,
  5810  			Before:          emptyVal,
  5811  			After:           nullVal,
  5812  			Schema:          emptySchema,
  5813  			RequiredReplace: cty.NewPathSet(),
  5814  			ExpectedOutput: `  # test_instance.example must be replaced
  5815  +/- resource "test_instance" "example" {}`,
  5816  		},
  5817  		"replace by request (delete first)": {
  5818  			Action:          plans.DeleteThenCreate,
  5819  			ActionReason:    plans.ResourceInstanceReplaceByRequest,
  5820  			Mode:            addrs.ManagedResourceMode,
  5821  			Before:          emptyVal,
  5822  			After:           nullVal,
  5823  			Schema:          emptySchema,
  5824  			RequiredReplace: cty.NewPathSet(),
  5825  			ExpectedOutput: `  # test_instance.example will be replaced, as requested
  5826  -/+ resource "test_instance" "example" {}`,
  5827  		},
  5828  		"replace by request (create first)": {
  5829  			Action:          plans.CreateThenDelete,
  5830  			ActionReason:    plans.ResourceInstanceReplaceByRequest,
  5831  			Mode:            addrs.ManagedResourceMode,
  5832  			Before:          emptyVal,
  5833  			After:           nullVal,
  5834  			Schema:          emptySchema,
  5835  			RequiredReplace: cty.NewPathSet(),
  5836  			ExpectedOutput: `  # test_instance.example will be replaced, as requested
  5837  +/- resource "test_instance" "example" {}`,
  5838  		},
  5839  		"replace because tainted (delete first)": {
  5840  			Action:          plans.DeleteThenCreate,
  5841  			ActionReason:    plans.ResourceInstanceReplaceBecauseTainted,
  5842  			Mode:            addrs.ManagedResourceMode,
  5843  			Before:          emptyVal,
  5844  			After:           nullVal,
  5845  			Schema:          emptySchema,
  5846  			RequiredReplace: cty.NewPathSet(),
  5847  			ExpectedOutput: `  # test_instance.example is tainted, so must be replaced
  5848  -/+ resource "test_instance" "example" {}`,
  5849  		},
  5850  		"replace because tainted (create first)": {
  5851  			Action:          plans.CreateThenDelete,
  5852  			ActionReason:    plans.ResourceInstanceReplaceBecauseTainted,
  5853  			Mode:            addrs.ManagedResourceMode,
  5854  			Before:          emptyVal,
  5855  			After:           nullVal,
  5856  			Schema:          emptySchema,
  5857  			RequiredReplace: cty.NewPathSet(),
  5858  			ExpectedOutput: `  # test_instance.example is tainted, so must be replaced
  5859  +/- resource "test_instance" "example" {}`,
  5860  		},
  5861  		"replace because cannot update (delete first)": {
  5862  			Action:          plans.DeleteThenCreate,
  5863  			ActionReason:    plans.ResourceInstanceReplaceBecauseCannotUpdate,
  5864  			Mode:            addrs.ManagedResourceMode,
  5865  			Before:          emptyVal,
  5866  			After:           nullVal,
  5867  			Schema:          emptySchema,
  5868  			RequiredReplace: cty.NewPathSet(),
  5869  			// This one has no special message, because the fuller explanation
  5870  			// typically appears inline as a "# forces replacement" comment.
  5871  			// (not shown here)
  5872  			ExpectedOutput: `  # test_instance.example must be replaced
  5873  -/+ resource "test_instance" "example" {}`,
  5874  		},
  5875  		"replace because cannot update (create first)": {
  5876  			Action:          plans.CreateThenDelete,
  5877  			ActionReason:    plans.ResourceInstanceReplaceBecauseCannotUpdate,
  5878  			Mode:            addrs.ManagedResourceMode,
  5879  			Before:          emptyVal,
  5880  			After:           nullVal,
  5881  			Schema:          emptySchema,
  5882  			RequiredReplace: cty.NewPathSet(),
  5883  			// This one has no special message, because the fuller explanation
  5884  			// typically appears inline as a "# forces replacement" comment.
  5885  			// (not shown here)
  5886  			ExpectedOutput: `  # test_instance.example must be replaced
  5887  +/- resource "test_instance" "example" {}`,
  5888  		},
  5889  	}
  5890  
  5891  	runTestCases(t, testCases)
  5892  }
  5893  
  5894  func TestResourceChange_sensitiveVariable(t *testing.T) {
  5895  	testCases := map[string]testCase{
  5896  		"creation": {
  5897  			Action: plans.Create,
  5898  			Mode:   addrs.ManagedResourceMode,
  5899  			Before: cty.NullVal(cty.EmptyObject),
  5900  			After: cty.ObjectVal(map[string]cty.Value{
  5901  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5902  				"ami": cty.StringVal("ami-123"),
  5903  				"map_key": cty.MapVal(map[string]cty.Value{
  5904  					"breakfast": cty.NumberIntVal(800),
  5905  					"dinner":    cty.NumberIntVal(2000),
  5906  				}),
  5907  				"map_whole": cty.MapVal(map[string]cty.Value{
  5908  					"breakfast": cty.StringVal("pizza"),
  5909  					"dinner":    cty.StringVal("pizza"),
  5910  				}),
  5911  				"list_field": cty.ListVal([]cty.Value{
  5912  					cty.StringVal("hello"),
  5913  					cty.StringVal("friends"),
  5914  					cty.StringVal("!"),
  5915  				}),
  5916  				"nested_block_list": cty.ListVal([]cty.Value{
  5917  					cty.ObjectVal(map[string]cty.Value{
  5918  						"an_attr": cty.StringVal("secretval"),
  5919  						"another": cty.StringVal("not secret"),
  5920  					}),
  5921  				}),
  5922  				"nested_block_set": cty.ListVal([]cty.Value{
  5923  					cty.ObjectVal(map[string]cty.Value{
  5924  						"an_attr": cty.StringVal("secretval"),
  5925  						"another": cty.StringVal("not secret"),
  5926  					}),
  5927  				}),
  5928  			}),
  5929  			AfterValMarks: []cty.PathValueMarks{
  5930  				{
  5931  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  5932  					Marks: cty.NewValueMarks(marks.Sensitive),
  5933  				},
  5934  				{
  5935  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}},
  5936  					Marks: cty.NewValueMarks(marks.Sensitive),
  5937  				},
  5938  				{
  5939  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  5940  					Marks: cty.NewValueMarks(marks.Sensitive),
  5941  				},
  5942  				{
  5943  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  5944  					Marks: cty.NewValueMarks(marks.Sensitive),
  5945  				},
  5946  				{
  5947  					// Nested blocks/sets will mark the whole set/block as sensitive
  5948  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_list"}},
  5949  					Marks: cty.NewValueMarks(marks.Sensitive),
  5950  				},
  5951  				{
  5952  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
  5953  					Marks: cty.NewValueMarks(marks.Sensitive),
  5954  				},
  5955  			},
  5956  			RequiredReplace: cty.NewPathSet(),
  5957  			Schema: &configschema.Block{
  5958  				Attributes: map[string]*configschema.Attribute{
  5959  					"id":         {Type: cty.String, Optional: true, Computed: true},
  5960  					"ami":        {Type: cty.String, Optional: true},
  5961  					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
  5962  					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
  5963  					"list_field": {Type: cty.List(cty.String), Optional: true},
  5964  				},
  5965  				BlockTypes: map[string]*configschema.NestedBlock{
  5966  					"nested_block_list": {
  5967  						Block: configschema.Block{
  5968  							Attributes: map[string]*configschema.Attribute{
  5969  								"an_attr": {Type: cty.String, Optional: true},
  5970  								"another": {Type: cty.String, Optional: true},
  5971  							},
  5972  						},
  5973  						Nesting: configschema.NestingList,
  5974  					},
  5975  					"nested_block_set": {
  5976  						Block: configschema.Block{
  5977  							Attributes: map[string]*configschema.Attribute{
  5978  								"an_attr": {Type: cty.String, Optional: true},
  5979  								"another": {Type: cty.String, Optional: true},
  5980  							},
  5981  						},
  5982  						Nesting: configschema.NestingSet,
  5983  					},
  5984  				},
  5985  			},
  5986  			ExpectedOutput: `  # test_instance.example will be created
  5987    + resource "test_instance" "example" {
  5988        + ami        = (sensitive value)
  5989        + id         = "i-02ae66f368e8518a9"
  5990        + list_field = [
  5991            + "hello",
  5992            + (sensitive value),
  5993            + "!",
  5994          ]
  5995        + map_key    = {
  5996            + "breakfast" = 800
  5997            + "dinner"    = (sensitive value)
  5998          }
  5999        + map_whole  = (sensitive value)
  6000  
  6001        + nested_block_list {
  6002            # At least one attribute in this block is (or was) sensitive,
  6003            # so its contents will not be displayed.
  6004          }
  6005  
  6006        + nested_block_set {
  6007            # At least one attribute in this block is (or was) sensitive,
  6008            # so its contents will not be displayed.
  6009          }
  6010      }`,
  6011  		},
  6012  		"in-place update - before sensitive": {
  6013  			Action: plans.Update,
  6014  			Mode:   addrs.ManagedResourceMode,
  6015  			Before: cty.ObjectVal(map[string]cty.Value{
  6016  				"id":          cty.StringVal("i-02ae66f368e8518a9"),
  6017  				"ami":         cty.StringVal("ami-BEFORE"),
  6018  				"special":     cty.BoolVal(true),
  6019  				"some_number": cty.NumberIntVal(1),
  6020  				"list_field": cty.ListVal([]cty.Value{
  6021  					cty.StringVal("hello"),
  6022  					cty.StringVal("friends"),
  6023  					cty.StringVal("!"),
  6024  				}),
  6025  				"map_key": cty.MapVal(map[string]cty.Value{
  6026  					"breakfast": cty.NumberIntVal(800),
  6027  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  6028  				}),
  6029  				"map_whole": cty.MapVal(map[string]cty.Value{
  6030  					"breakfast": cty.StringVal("pizza"),
  6031  					"dinner":    cty.StringVal("pizza"),
  6032  				}),
  6033  				"nested_block": cty.ListVal([]cty.Value{
  6034  					cty.ObjectVal(map[string]cty.Value{
  6035  						"an_attr": cty.StringVal("secretval"),
  6036  					}),
  6037  				}),
  6038  				"nested_block_set": cty.ListVal([]cty.Value{
  6039  					cty.ObjectVal(map[string]cty.Value{
  6040  						"an_attr": cty.StringVal("secretval"),
  6041  					}),
  6042  				}),
  6043  			}),
  6044  			After: cty.ObjectVal(map[string]cty.Value{
  6045  				"id":          cty.StringVal("i-02ae66f368e8518a9"),
  6046  				"ami":         cty.StringVal("ami-AFTER"),
  6047  				"special":     cty.BoolVal(false),
  6048  				"some_number": cty.NumberIntVal(2),
  6049  				"list_field": cty.ListVal([]cty.Value{
  6050  					cty.StringVal("hello"),
  6051  					cty.StringVal("friends"),
  6052  					cty.StringVal("."),
  6053  				}),
  6054  				"map_key": cty.MapVal(map[string]cty.Value{
  6055  					"breakfast": cty.NumberIntVal(800),
  6056  					"dinner":    cty.NumberIntVal(1900),
  6057  				}),
  6058  				"map_whole": cty.MapVal(map[string]cty.Value{
  6059  					"breakfast": cty.StringVal("cereal"),
  6060  					"dinner":    cty.StringVal("pizza"),
  6061  				}),
  6062  				"nested_block": cty.ListVal([]cty.Value{
  6063  					cty.ObjectVal(map[string]cty.Value{
  6064  						"an_attr": cty.StringVal("changed"),
  6065  					}),
  6066  				}),
  6067  				"nested_block_set": cty.ListVal([]cty.Value{
  6068  					cty.ObjectVal(map[string]cty.Value{
  6069  						"an_attr": cty.StringVal("changed"),
  6070  					}),
  6071  				}),
  6072  			}),
  6073  			BeforeValMarks: []cty.PathValueMarks{
  6074  				{
  6075  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  6076  					Marks: cty.NewValueMarks(marks.Sensitive),
  6077  				},
  6078  				{
  6079  					Path:  cty.Path{cty.GetAttrStep{Name: "special"}},
  6080  					Marks: cty.NewValueMarks(marks.Sensitive),
  6081  				},
  6082  				{
  6083  					Path:  cty.Path{cty.GetAttrStep{Name: "some_number"}},
  6084  					Marks: cty.NewValueMarks(marks.Sensitive),
  6085  				},
  6086  				{
  6087  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}},
  6088  					Marks: cty.NewValueMarks(marks.Sensitive),
  6089  				},
  6090  				{
  6091  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  6092  					Marks: cty.NewValueMarks(marks.Sensitive),
  6093  				},
  6094  				{
  6095  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  6096  					Marks: cty.NewValueMarks(marks.Sensitive),
  6097  				},
  6098  				{
  6099  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block"}},
  6100  					Marks: cty.NewValueMarks(marks.Sensitive),
  6101  				},
  6102  				{
  6103  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
  6104  					Marks: cty.NewValueMarks(marks.Sensitive),
  6105  				},
  6106  			},
  6107  			RequiredReplace: cty.NewPathSet(),
  6108  			Schema: &configschema.Block{
  6109  				Attributes: map[string]*configschema.Attribute{
  6110  					"id":          {Type: cty.String, Optional: true, Computed: true},
  6111  					"ami":         {Type: cty.String, Optional: true},
  6112  					"list_field":  {Type: cty.List(cty.String), Optional: true},
  6113  					"special":     {Type: cty.Bool, Optional: true},
  6114  					"some_number": {Type: cty.Number, Optional: true},
  6115  					"map_key":     {Type: cty.Map(cty.Number), Optional: true},
  6116  					"map_whole":   {Type: cty.Map(cty.String), Optional: true},
  6117  				},
  6118  				BlockTypes: map[string]*configschema.NestedBlock{
  6119  					"nested_block": {
  6120  						Block: configschema.Block{
  6121  							Attributes: map[string]*configschema.Attribute{
  6122  								"an_attr": {Type: cty.String, Optional: true},
  6123  							},
  6124  						},
  6125  						Nesting: configschema.NestingList,
  6126  					},
  6127  					"nested_block_set": {
  6128  						Block: configschema.Block{
  6129  							Attributes: map[string]*configschema.Attribute{
  6130  								"an_attr": {Type: cty.String, Optional: true},
  6131  							},
  6132  						},
  6133  						Nesting: configschema.NestingSet,
  6134  					},
  6135  				},
  6136  			},
  6137  			ExpectedOutput: `  # test_instance.example will be updated in-place
  6138    ~ resource "test_instance" "example" {
  6139        # Warning: this attribute value will no longer be marked as sensitive
  6140        # after applying this change.
  6141        ~ ami         = (sensitive value)
  6142          id          = "i-02ae66f368e8518a9"
  6143        ~ list_field  = [
  6144              # (1 unchanged element hidden)
  6145              "friends",
  6146            - (sensitive value),
  6147            + ".",
  6148          ]
  6149        ~ map_key     = {
  6150            # Warning: this attribute value will no longer be marked as sensitive
  6151            # after applying this change.
  6152            ~ "dinner"    = (sensitive value)
  6153              # (1 unchanged element hidden)
  6154          }
  6155        # Warning: this attribute value will no longer be marked as sensitive
  6156        # after applying this change.
  6157        ~ map_whole   = (sensitive value)
  6158        # Warning: this attribute value will no longer be marked as sensitive
  6159        # after applying this change.
  6160        ~ some_number = (sensitive value)
  6161        # Warning: this attribute value will no longer be marked as sensitive
  6162        # after applying this change.
  6163        ~ special     = (sensitive value)
  6164  
  6165        # Warning: this block will no longer be marked as sensitive
  6166        # after applying this change.
  6167        ~ nested_block {
  6168            # At least one attribute in this block is (or was) sensitive,
  6169            # so its contents will not be displayed.
  6170          }
  6171  
  6172        - nested_block_set {
  6173            # At least one attribute in this block is (or was) sensitive,
  6174            # so its contents will not be displayed.
  6175          }
  6176        + nested_block_set {
  6177            + an_attr = "changed"
  6178          }
  6179      }`,
  6180  		},
  6181  		"in-place update - after sensitive": {
  6182  			Action: plans.Update,
  6183  			Mode:   addrs.ManagedResourceMode,
  6184  			Before: cty.ObjectVal(map[string]cty.Value{
  6185  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  6186  				"list_field": cty.ListVal([]cty.Value{
  6187  					cty.StringVal("hello"),
  6188  					cty.StringVal("friends"),
  6189  				}),
  6190  				"map_key": cty.MapVal(map[string]cty.Value{
  6191  					"breakfast": cty.NumberIntVal(800),
  6192  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  6193  				}),
  6194  				"map_whole": cty.MapVal(map[string]cty.Value{
  6195  					"breakfast": cty.StringVal("pizza"),
  6196  					"dinner":    cty.StringVal("pizza"),
  6197  				}),
  6198  				"nested_block_single": cty.ObjectVal(map[string]cty.Value{
  6199  					"an_attr": cty.StringVal("original"),
  6200  				}),
  6201  			}),
  6202  			After: cty.ObjectVal(map[string]cty.Value{
  6203  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  6204  				"list_field": cty.ListVal([]cty.Value{
  6205  					cty.StringVal("goodbye"),
  6206  					cty.StringVal("friends"),
  6207  				}),
  6208  				"map_key": cty.MapVal(map[string]cty.Value{
  6209  					"breakfast": cty.NumberIntVal(700),
  6210  					"dinner":    cty.NumberIntVal(2100), // sensitive key
  6211  				}),
  6212  				"map_whole": cty.MapVal(map[string]cty.Value{
  6213  					"breakfast": cty.StringVal("cereal"),
  6214  					"dinner":    cty.StringVal("pizza"),
  6215  				}),
  6216  				"nested_block_single": cty.ObjectVal(map[string]cty.Value{
  6217  					"an_attr": cty.StringVal("changed"),
  6218  				}),
  6219  			}),
  6220  			AfterValMarks: []cty.PathValueMarks{
  6221  				{
  6222  					Path:  cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("address")}},
  6223  					Marks: cty.NewValueMarks(marks.Sensitive),
  6224  				},
  6225  				{
  6226  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
  6227  					Marks: cty.NewValueMarks(marks.Sensitive),
  6228  				},
  6229  				{
  6230  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  6231  					Marks: cty.NewValueMarks(marks.Sensitive),
  6232  				},
  6233  				{
  6234  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  6235  					Marks: cty.NewValueMarks(marks.Sensitive),
  6236  				},
  6237  				{
  6238  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_single"}},
  6239  					Marks: cty.NewValueMarks(marks.Sensitive),
  6240  				},
  6241  			},
  6242  			RequiredReplace: cty.NewPathSet(),
  6243  			Schema: &configschema.Block{
  6244  				Attributes: map[string]*configschema.Attribute{
  6245  					"id":         {Type: cty.String, Optional: true, Computed: true},
  6246  					"list_field": {Type: cty.List(cty.String), Optional: true},
  6247  					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
  6248  					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
  6249  				},
  6250  				BlockTypes: map[string]*configschema.NestedBlock{
  6251  					"nested_block_single": {
  6252  						Block: configschema.Block{
  6253  							Attributes: map[string]*configschema.Attribute{
  6254  								"an_attr": {Type: cty.String, Optional: true},
  6255  							},
  6256  						},
  6257  						Nesting: configschema.NestingSingle,
  6258  					},
  6259  				},
  6260  			},
  6261  			ExpectedOutput: `  # test_instance.example will be updated in-place
  6262    ~ resource "test_instance" "example" {
  6263          id         = "i-02ae66f368e8518a9"
  6264        ~ list_field = [
  6265            - "hello",
  6266            + (sensitive value),
  6267              "friends",
  6268          ]
  6269        ~ map_key    = {
  6270            ~ "breakfast" = 800 -> 700
  6271            # Warning: this attribute value will be marked as sensitive and will not
  6272            # display in UI output after applying this change.
  6273            ~ "dinner"    = (sensitive value)
  6274          }
  6275        # Warning: this attribute value will be marked as sensitive and will not
  6276        # display in UI output after applying this change.
  6277        ~ map_whole  = (sensitive value)
  6278  
  6279        # Warning: this block will be marked as sensitive and will not
  6280        # display in UI output after applying this change.
  6281        ~ nested_block_single {
  6282            # At least one attribute in this block is (or was) sensitive,
  6283            # so its contents will not be displayed.
  6284          }
  6285      }`,
  6286  		},
  6287  		"in-place update - both sensitive": {
  6288  			Action: plans.Update,
  6289  			Mode:   addrs.ManagedResourceMode,
  6290  			Before: cty.ObjectVal(map[string]cty.Value{
  6291  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  6292  				"ami": cty.StringVal("ami-BEFORE"),
  6293  				"list_field": cty.ListVal([]cty.Value{
  6294  					cty.StringVal("hello"),
  6295  					cty.StringVal("friends"),
  6296  				}),
  6297  				"map_key": cty.MapVal(map[string]cty.Value{
  6298  					"breakfast": cty.NumberIntVal(800),
  6299  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  6300  				}),
  6301  				"map_whole": cty.MapVal(map[string]cty.Value{
  6302  					"breakfast": cty.StringVal("pizza"),
  6303  					"dinner":    cty.StringVal("pizza"),
  6304  				}),
  6305  				"nested_block_map": cty.MapVal(map[string]cty.Value{
  6306  					"foo": cty.ObjectVal(map[string]cty.Value{
  6307  						"an_attr": cty.StringVal("original"),
  6308  					}),
  6309  				}),
  6310  			}),
  6311  			After: cty.ObjectVal(map[string]cty.Value{
  6312  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  6313  				"ami": cty.StringVal("ami-AFTER"),
  6314  				"list_field": cty.ListVal([]cty.Value{
  6315  					cty.StringVal("goodbye"),
  6316  					cty.StringVal("friends"),
  6317  				}),
  6318  				"map_key": cty.MapVal(map[string]cty.Value{
  6319  					"breakfast": cty.NumberIntVal(800),
  6320  					"dinner":    cty.NumberIntVal(1800), // sensitive key
  6321  				}),
  6322  				"map_whole": cty.MapVal(map[string]cty.Value{
  6323  					"breakfast": cty.StringVal("cereal"),
  6324  					"dinner":    cty.StringVal("pizza"),
  6325  				}),
  6326  				"nested_block_map": cty.MapVal(map[string]cty.Value{
  6327  					"foo": cty.ObjectVal(map[string]cty.Value{
  6328  						"an_attr": cty.UnknownVal(cty.String),
  6329  					}),
  6330  				}),
  6331  			}),
  6332  			BeforeValMarks: []cty.PathValueMarks{
  6333  				{
  6334  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  6335  					Marks: cty.NewValueMarks(marks.Sensitive),
  6336  				},
  6337  				{
  6338  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
  6339  					Marks: cty.NewValueMarks(marks.Sensitive),
  6340  				},
  6341  				{
  6342  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  6343  					Marks: cty.NewValueMarks(marks.Sensitive),
  6344  				},
  6345  				{
  6346  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  6347  					Marks: cty.NewValueMarks(marks.Sensitive),
  6348  				},
  6349  				{
  6350  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_map"}},
  6351  					Marks: cty.NewValueMarks(marks.Sensitive),
  6352  				},
  6353  			},
  6354  			AfterValMarks: []cty.PathValueMarks{
  6355  				{
  6356  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  6357  					Marks: cty.NewValueMarks(marks.Sensitive),
  6358  				},
  6359  				{
  6360  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
  6361  					Marks: cty.NewValueMarks(marks.Sensitive),
  6362  				},
  6363  				{
  6364  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  6365  					Marks: cty.NewValueMarks(marks.Sensitive),
  6366  				},
  6367  				{
  6368  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  6369  					Marks: cty.NewValueMarks(marks.Sensitive),
  6370  				},
  6371  				{
  6372  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_map"}},
  6373  					Marks: cty.NewValueMarks(marks.Sensitive),
  6374  				},
  6375  			},
  6376  			RequiredReplace: cty.NewPathSet(),
  6377  			Schema: &configschema.Block{
  6378  				Attributes: map[string]*configschema.Attribute{
  6379  					"id":         {Type: cty.String, Optional: true, Computed: true},
  6380  					"ami":        {Type: cty.String, Optional: true},
  6381  					"list_field": {Type: cty.List(cty.String), Optional: true},
  6382  					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
  6383  					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
  6384  				},
  6385  				BlockTypes: map[string]*configschema.NestedBlock{
  6386  					"nested_block_map": {
  6387  						Block: configschema.Block{
  6388  							Attributes: map[string]*configschema.Attribute{
  6389  								"an_attr": {Type: cty.String, Optional: true},
  6390  							},
  6391  						},
  6392  						Nesting: configschema.NestingMap,
  6393  					},
  6394  				},
  6395  			},
  6396  			ExpectedOutput: `  # test_instance.example will be updated in-place
  6397    ~ resource "test_instance" "example" {
  6398        ~ ami        = (sensitive value)
  6399          id         = "i-02ae66f368e8518a9"
  6400        ~ list_field = [
  6401            - (sensitive value),
  6402            + (sensitive value),
  6403              "friends",
  6404          ]
  6405        ~ map_key    = {
  6406            ~ "dinner"    = (sensitive value)
  6407              # (1 unchanged element hidden)
  6408          }
  6409        ~ map_whole  = (sensitive value)
  6410  
  6411        ~ nested_block_map "foo" {
  6412            # At least one attribute in this block is (or was) sensitive,
  6413            # so its contents will not be displayed.
  6414          }
  6415      }`,
  6416  		},
  6417  		"in-place update - value unchanged, sensitivity changes": {
  6418  			Action: plans.Update,
  6419  			Mode:   addrs.ManagedResourceMode,
  6420  			Before: cty.ObjectVal(map[string]cty.Value{
  6421  				"id":          cty.StringVal("i-02ae66f368e8518a9"),
  6422  				"ami":         cty.StringVal("ami-BEFORE"),
  6423  				"special":     cty.BoolVal(true),
  6424  				"some_number": cty.NumberIntVal(1),
  6425  				"list_field": cty.ListVal([]cty.Value{
  6426  					cty.StringVal("hello"),
  6427  					cty.StringVal("friends"),
  6428  					cty.StringVal("!"),
  6429  				}),
  6430  				"map_key": cty.MapVal(map[string]cty.Value{
  6431  					"breakfast": cty.NumberIntVal(800),
  6432  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  6433  				}),
  6434  				"map_whole": cty.MapVal(map[string]cty.Value{
  6435  					"breakfast": cty.StringVal("pizza"),
  6436  					"dinner":    cty.StringVal("pizza"),
  6437  				}),
  6438  				"nested_block": cty.ListVal([]cty.Value{
  6439  					cty.ObjectVal(map[string]cty.Value{
  6440  						"an_attr": cty.StringVal("secretval"),
  6441  					}),
  6442  				}),
  6443  				"nested_block_set": cty.ListVal([]cty.Value{
  6444  					cty.ObjectVal(map[string]cty.Value{
  6445  						"an_attr": cty.StringVal("secretval"),
  6446  					}),
  6447  				}),
  6448  			}),
  6449  			After: cty.ObjectVal(map[string]cty.Value{
  6450  				"id":          cty.StringVal("i-02ae66f368e8518a9"),
  6451  				"ami":         cty.StringVal("ami-BEFORE"),
  6452  				"special":     cty.BoolVal(true),
  6453  				"some_number": cty.NumberIntVal(1),
  6454  				"list_field": cty.ListVal([]cty.Value{
  6455  					cty.StringVal("hello"),
  6456  					cty.StringVal("friends"),
  6457  					cty.StringVal("!"),
  6458  				}),
  6459  				"map_key": cty.MapVal(map[string]cty.Value{
  6460  					"breakfast": cty.NumberIntVal(800),
  6461  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  6462  				}),
  6463  				"map_whole": cty.MapVal(map[string]cty.Value{
  6464  					"breakfast": cty.StringVal("pizza"),
  6465  					"dinner":    cty.StringVal("pizza"),
  6466  				}),
  6467  				"nested_block": cty.ListVal([]cty.Value{
  6468  					cty.ObjectVal(map[string]cty.Value{
  6469  						"an_attr": cty.StringVal("secretval"),
  6470  					}),
  6471  				}),
  6472  				"nested_block_set": cty.ListVal([]cty.Value{
  6473  					cty.ObjectVal(map[string]cty.Value{
  6474  						"an_attr": cty.StringVal("secretval"),
  6475  					}),
  6476  				}),
  6477  			}),
  6478  			BeforeValMarks: []cty.PathValueMarks{
  6479  				{
  6480  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  6481  					Marks: cty.NewValueMarks(marks.Sensitive),
  6482  				},
  6483  				{
  6484  					Path:  cty.Path{cty.GetAttrStep{Name: "special"}},
  6485  					Marks: cty.NewValueMarks(marks.Sensitive),
  6486  				},
  6487  				{
  6488  					Path:  cty.Path{cty.GetAttrStep{Name: "some_number"}},
  6489  					Marks: cty.NewValueMarks(marks.Sensitive),
  6490  				},
  6491  				{
  6492  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}},
  6493  					Marks: cty.NewValueMarks(marks.Sensitive),
  6494  				},
  6495  				{
  6496  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  6497  					Marks: cty.NewValueMarks(marks.Sensitive),
  6498  				},
  6499  				{
  6500  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  6501  					Marks: cty.NewValueMarks(marks.Sensitive),
  6502  				},
  6503  				{
  6504  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block"}},
  6505  					Marks: cty.NewValueMarks(marks.Sensitive),
  6506  				},
  6507  				{
  6508  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
  6509  					Marks: cty.NewValueMarks(marks.Sensitive),
  6510  				},
  6511  			},
  6512  			RequiredReplace: cty.NewPathSet(),
  6513  			Schema: &configschema.Block{
  6514  				Attributes: map[string]*configschema.Attribute{
  6515  					"id":          {Type: cty.String, Optional: true, Computed: true},
  6516  					"ami":         {Type: cty.String, Optional: true},
  6517  					"list_field":  {Type: cty.List(cty.String), Optional: true},
  6518  					"special":     {Type: cty.Bool, Optional: true},
  6519  					"some_number": {Type: cty.Number, Optional: true},
  6520  					"map_key":     {Type: cty.Map(cty.Number), Optional: true},
  6521  					"map_whole":   {Type: cty.Map(cty.String), Optional: true},
  6522  				},
  6523  				BlockTypes: map[string]*configschema.NestedBlock{
  6524  					"nested_block": {
  6525  						Block: configschema.Block{
  6526  							Attributes: map[string]*configschema.Attribute{
  6527  								"an_attr": {Type: cty.String, Optional: true},
  6528  							},
  6529  						},
  6530  						Nesting: configschema.NestingList,
  6531  					},
  6532  					"nested_block_set": {
  6533  						Block: configschema.Block{
  6534  							Attributes: map[string]*configschema.Attribute{
  6535  								"an_attr": {Type: cty.String, Optional: true},
  6536  							},
  6537  						},
  6538  						Nesting: configschema.NestingSet,
  6539  					},
  6540  				},
  6541  			},
  6542  			ExpectedOutput: `  # test_instance.example will be updated in-place
  6543    ~ resource "test_instance" "example" {
  6544        # Warning: this attribute value will no longer be marked as sensitive
  6545        # after applying this change. The value is unchanged.
  6546        ~ ami         = (sensitive value)
  6547          id          = "i-02ae66f368e8518a9"
  6548        ~ list_field  = [
  6549              # (1 unchanged element hidden)
  6550              "friends",
  6551            # Warning: this attribute value will no longer be marked as sensitive
  6552            # after applying this change. The value is unchanged.
  6553            ~ (sensitive value),
  6554          ]
  6555        ~ map_key     = {
  6556            # Warning: this attribute value will no longer be marked as sensitive
  6557            # after applying this change. The value is unchanged.
  6558            ~ "dinner"    = (sensitive value)
  6559              # (1 unchanged element hidden)
  6560          }
  6561        # Warning: this attribute value will no longer be marked as sensitive
  6562        # after applying this change. The value is unchanged.
  6563        ~ map_whole   = (sensitive value)
  6564        # Warning: this attribute value will no longer be marked as sensitive
  6565        # after applying this change. The value is unchanged.
  6566        ~ some_number = (sensitive value)
  6567        # Warning: this attribute value will no longer be marked as sensitive
  6568        # after applying this change. The value is unchanged.
  6569        ~ special     = (sensitive value)
  6570  
  6571        # Warning: this block will no longer be marked as sensitive
  6572        # after applying this change.
  6573        ~ nested_block {
  6574            # At least one attribute in this block is (or was) sensitive,
  6575            # so its contents will not be displayed.
  6576          }
  6577  
  6578        # Warning: this block will no longer be marked as sensitive
  6579        # after applying this change.
  6580        ~ nested_block_set {
  6581            # At least one attribute in this block is (or was) sensitive,
  6582            # so its contents will not be displayed.
  6583          }
  6584      }`,
  6585  		},
  6586  		"deletion": {
  6587  			Action: plans.Delete,
  6588  			Mode:   addrs.ManagedResourceMode,
  6589  			Before: cty.ObjectVal(map[string]cty.Value{
  6590  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  6591  				"ami": cty.StringVal("ami-BEFORE"),
  6592  				"list_field": cty.ListVal([]cty.Value{
  6593  					cty.StringVal("hello"),
  6594  					cty.StringVal("friends"),
  6595  				}),
  6596  				"map_key": cty.MapVal(map[string]cty.Value{
  6597  					"breakfast": cty.NumberIntVal(800),
  6598  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  6599  				}),
  6600  				"map_whole": cty.MapVal(map[string]cty.Value{
  6601  					"breakfast": cty.StringVal("pizza"),
  6602  					"dinner":    cty.StringVal("pizza"),
  6603  				}),
  6604  				"nested_block": cty.ListVal([]cty.Value{
  6605  					cty.ObjectVal(map[string]cty.Value{
  6606  						"an_attr": cty.StringVal("secret"),
  6607  						"another": cty.StringVal("not secret"),
  6608  					}),
  6609  				}),
  6610  				"nested_block_set": cty.ListVal([]cty.Value{
  6611  					cty.ObjectVal(map[string]cty.Value{
  6612  						"an_attr": cty.StringVal("secret"),
  6613  						"another": cty.StringVal("not secret"),
  6614  					}),
  6615  				}),
  6616  			}),
  6617  			After: cty.NullVal(cty.EmptyObject),
  6618  			BeforeValMarks: []cty.PathValueMarks{
  6619  				{
  6620  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  6621  					Marks: cty.NewValueMarks(marks.Sensitive),
  6622  				},
  6623  				{
  6624  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}},
  6625  					Marks: cty.NewValueMarks(marks.Sensitive),
  6626  				},
  6627  				{
  6628  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  6629  					Marks: cty.NewValueMarks(marks.Sensitive),
  6630  				},
  6631  				{
  6632  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  6633  					Marks: cty.NewValueMarks(marks.Sensitive),
  6634  				},
  6635  				{
  6636  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block"}},
  6637  					Marks: cty.NewValueMarks(marks.Sensitive),
  6638  				},
  6639  				{
  6640  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
  6641  					Marks: cty.NewValueMarks(marks.Sensitive),
  6642  				},
  6643  			},
  6644  			RequiredReplace: cty.NewPathSet(),
  6645  			Schema: &configschema.Block{
  6646  				Attributes: map[string]*configschema.Attribute{
  6647  					"id":         {Type: cty.String, Optional: true, Computed: true},
  6648  					"ami":        {Type: cty.String, Optional: true},
  6649  					"list_field": {Type: cty.List(cty.String), Optional: true},
  6650  					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
  6651  					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
  6652  				},
  6653  				BlockTypes: map[string]*configschema.NestedBlock{
  6654  					"nested_block_set": {
  6655  						Block: configschema.Block{
  6656  							Attributes: map[string]*configschema.Attribute{
  6657  								"an_attr": {Type: cty.String, Optional: true},
  6658  								"another": {Type: cty.String, Optional: true},
  6659  							},
  6660  						},
  6661  						Nesting: configschema.NestingSet,
  6662  					},
  6663  				},
  6664  			},
  6665  			ExpectedOutput: `  # test_instance.example will be destroyed
  6666    - resource "test_instance" "example" {
  6667        - ami        = (sensitive value) -> null
  6668        - id         = "i-02ae66f368e8518a9" -> null
  6669        - list_field = [
  6670            - "hello",
  6671            - (sensitive value),
  6672          ] -> null
  6673        - map_key    = {
  6674            - "breakfast" = 800
  6675            - "dinner"    = (sensitive value)
  6676          } -> null
  6677        - map_whole  = (sensitive value) -> null
  6678  
  6679        - nested_block_set {
  6680            # At least one attribute in this block is (or was) sensitive,
  6681            # so its contents will not be displayed.
  6682          }
  6683      }`,
  6684  		},
  6685  		"update with sensitive value forcing replacement": {
  6686  			Action: plans.DeleteThenCreate,
  6687  			Mode:   addrs.ManagedResourceMode,
  6688  			Before: cty.ObjectVal(map[string]cty.Value{
  6689  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  6690  				"ami": cty.StringVal("ami-BEFORE"),
  6691  				"nested_block_set": cty.SetVal([]cty.Value{
  6692  					cty.ObjectVal(map[string]cty.Value{
  6693  						"an_attr": cty.StringVal("secret"),
  6694  					}),
  6695  				}),
  6696  			}),
  6697  			After: cty.ObjectVal(map[string]cty.Value{
  6698  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  6699  				"ami": cty.StringVal("ami-AFTER"),
  6700  				"nested_block_set": cty.SetVal([]cty.Value{
  6701  					cty.ObjectVal(map[string]cty.Value{
  6702  						"an_attr": cty.StringVal("changed"),
  6703  					}),
  6704  				}),
  6705  			}),
  6706  			BeforeValMarks: []cty.PathValueMarks{
  6707  				{
  6708  					Path:  cty.GetAttrPath("ami"),
  6709  					Marks: cty.NewValueMarks(marks.Sensitive),
  6710  				},
  6711  				{
  6712  					Path:  cty.GetAttrPath("nested_block_set"),
  6713  					Marks: cty.NewValueMarks(marks.Sensitive),
  6714  				},
  6715  			},
  6716  			AfterValMarks: []cty.PathValueMarks{
  6717  				{
  6718  					Path:  cty.GetAttrPath("ami"),
  6719  					Marks: cty.NewValueMarks(marks.Sensitive),
  6720  				},
  6721  				{
  6722  					Path:  cty.GetAttrPath("nested_block_set"),
  6723  					Marks: cty.NewValueMarks(marks.Sensitive),
  6724  				},
  6725  			},
  6726  			Schema: &configschema.Block{
  6727  				Attributes: map[string]*configschema.Attribute{
  6728  					"id":  {Type: cty.String, Optional: true, Computed: true},
  6729  					"ami": {Type: cty.String, Optional: true},
  6730  				},
  6731  				BlockTypes: map[string]*configschema.NestedBlock{
  6732  					"nested_block_set": {
  6733  						Block: configschema.Block{
  6734  							Attributes: map[string]*configschema.Attribute{
  6735  								"an_attr": {Type: cty.String, Required: true},
  6736  							},
  6737  						},
  6738  						Nesting: configschema.NestingSet,
  6739  					},
  6740  				},
  6741  			},
  6742  			RequiredReplace: cty.NewPathSet(
  6743  				cty.GetAttrPath("ami"),
  6744  				cty.GetAttrPath("nested_block_set"),
  6745  			),
  6746  			ExpectedOutput: `  # test_instance.example must be replaced
  6747  -/+ resource "test_instance" "example" {
  6748        ~ ami = (sensitive value) # forces replacement
  6749          id  = "i-02ae66f368e8518a9"
  6750  
  6751        - nested_block_set { # forces replacement
  6752            # At least one attribute in this block is (or was) sensitive,
  6753            # so its contents will not be displayed.
  6754          }
  6755        + nested_block_set { # forces replacement
  6756            # At least one attribute in this block is (or was) sensitive,
  6757            # so its contents will not be displayed.
  6758          }
  6759      }`,
  6760  		},
  6761  		"update with sensitive attribute forcing replacement": {
  6762  			Action: plans.DeleteThenCreate,
  6763  			Mode:   addrs.ManagedResourceMode,
  6764  			Before: cty.ObjectVal(map[string]cty.Value{
  6765  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  6766  				"ami": cty.StringVal("ami-BEFORE"),
  6767  			}),
  6768  			After: cty.ObjectVal(map[string]cty.Value{
  6769  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  6770  				"ami": cty.StringVal("ami-AFTER"),
  6771  			}),
  6772  			Schema: &configschema.Block{
  6773  				Attributes: map[string]*configschema.Attribute{
  6774  					"id":  {Type: cty.String, Optional: true, Computed: true},
  6775  					"ami": {Type: cty.String, Optional: true, Computed: true, Sensitive: true},
  6776  				},
  6777  			},
  6778  			RequiredReplace: cty.NewPathSet(
  6779  				cty.GetAttrPath("ami"),
  6780  			),
  6781  			ExpectedOutput: `  # test_instance.example must be replaced
  6782  -/+ resource "test_instance" "example" {
  6783        ~ ami = (sensitive value) # forces replacement
  6784          id  = "i-02ae66f368e8518a9"
  6785      }`,
  6786  		},
  6787  		"update with sensitive nested type attribute forcing replacement": {
  6788  			Action: plans.DeleteThenCreate,
  6789  			Mode:   addrs.ManagedResourceMode,
  6790  			Before: cty.ObjectVal(map[string]cty.Value{
  6791  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  6792  				"conn_info": cty.ObjectVal(map[string]cty.Value{
  6793  					"user":     cty.StringVal("not-secret"),
  6794  					"password": cty.StringVal("top-secret"),
  6795  				}),
  6796  			}),
  6797  			After: cty.ObjectVal(map[string]cty.Value{
  6798  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  6799  				"conn_info": cty.ObjectVal(map[string]cty.Value{
  6800  					"user":     cty.StringVal("not-secret"),
  6801  					"password": cty.StringVal("new-secret"),
  6802  				}),
  6803  			}),
  6804  			Schema: &configschema.Block{
  6805  				Attributes: map[string]*configschema.Attribute{
  6806  					"id": {Type: cty.String, Optional: true, Computed: true},
  6807  					"conn_info": {
  6808  						NestedType: &configschema.Object{
  6809  							Nesting: configschema.NestingSingle,
  6810  							Attributes: map[string]*configschema.Attribute{
  6811  								"user":     {Type: cty.String, Optional: true},
  6812  								"password": {Type: cty.String, Optional: true, Sensitive: true},
  6813  							},
  6814  						},
  6815  					},
  6816  				},
  6817  			},
  6818  			RequiredReplace: cty.NewPathSet(
  6819  				cty.GetAttrPath("conn_info"),
  6820  				cty.GetAttrPath("password"),
  6821  			),
  6822  			ExpectedOutput: `  # test_instance.example must be replaced
  6823  -/+ resource "test_instance" "example" {
  6824        ~ conn_info = { # forces replacement
  6825            ~ password = (sensitive value)
  6826              # (1 unchanged attribute hidden)
  6827          }
  6828          id        = "i-02ae66f368e8518a9"
  6829      }`,
  6830  		},
  6831  	}
  6832  	runTestCases(t, testCases)
  6833  }
  6834  
  6835  func TestResourceChange_moved(t *testing.T) {
  6836  	prevRunAddr := addrs.Resource{
  6837  		Mode: addrs.ManagedResourceMode,
  6838  		Type: "test_instance",
  6839  		Name: "previous",
  6840  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
  6841  
  6842  	testCases := map[string]testCase{
  6843  		"moved and updated": {
  6844  			PrevRunAddr: prevRunAddr,
  6845  			Action:      plans.Update,
  6846  			Mode:        addrs.ManagedResourceMode,
  6847  			Before: cty.ObjectVal(map[string]cty.Value{
  6848  				"id":  cty.StringVal("12345"),
  6849  				"foo": cty.StringVal("hello"),
  6850  				"bar": cty.StringVal("baz"),
  6851  			}),
  6852  			After: cty.ObjectVal(map[string]cty.Value{
  6853  				"id":  cty.StringVal("12345"),
  6854  				"foo": cty.StringVal("hello"),
  6855  				"bar": cty.StringVal("boop"),
  6856  			}),
  6857  			Schema: &configschema.Block{
  6858  				Attributes: map[string]*configschema.Attribute{
  6859  					"id":  {Type: cty.String, Computed: true},
  6860  					"foo": {Type: cty.String, Optional: true},
  6861  					"bar": {Type: cty.String, Optional: true},
  6862  				},
  6863  			},
  6864  			RequiredReplace: cty.NewPathSet(),
  6865  			ExpectedOutput: `  # test_instance.example will be updated in-place
  6866    # (moved from test_instance.previous)
  6867    ~ resource "test_instance" "example" {
  6868        ~ bar = "baz" -> "boop"
  6869          id  = "12345"
  6870          # (1 unchanged attribute hidden)
  6871      }`,
  6872  		},
  6873  		"moved without changes": {
  6874  			PrevRunAddr: prevRunAddr,
  6875  			Action:      plans.NoOp,
  6876  			Mode:        addrs.ManagedResourceMode,
  6877  			Before: cty.ObjectVal(map[string]cty.Value{
  6878  				"id":  cty.StringVal("12345"),
  6879  				"foo": cty.StringVal("hello"),
  6880  				"bar": cty.StringVal("baz"),
  6881  			}),
  6882  			After: cty.ObjectVal(map[string]cty.Value{
  6883  				"id":  cty.StringVal("12345"),
  6884  				"foo": cty.StringVal("hello"),
  6885  				"bar": cty.StringVal("baz"),
  6886  			}),
  6887  			Schema: &configschema.Block{
  6888  				Attributes: map[string]*configschema.Attribute{
  6889  					"id":  {Type: cty.String, Computed: true},
  6890  					"foo": {Type: cty.String, Optional: true},
  6891  					"bar": {Type: cty.String, Optional: true},
  6892  				},
  6893  			},
  6894  			RequiredReplace: cty.NewPathSet(),
  6895  			ExpectedOutput: `  # test_instance.previous has moved to test_instance.example
  6896      resource "test_instance" "example" {
  6897          id  = "12345"
  6898          # (2 unchanged attributes hidden)
  6899      }`,
  6900  		},
  6901  	}
  6902  
  6903  	runTestCases(t, testCases)
  6904  }
  6905  
  6906  type testCase struct {
  6907  	Action          plans.Action
  6908  	ActionReason    plans.ResourceInstanceChangeActionReason
  6909  	ModuleInst      addrs.ModuleInstance
  6910  	Mode            addrs.ResourceMode
  6911  	InstanceKey     addrs.InstanceKey
  6912  	DeposedKey      states.DeposedKey
  6913  	Before          cty.Value
  6914  	BeforeValMarks  []cty.PathValueMarks
  6915  	AfterValMarks   []cty.PathValueMarks
  6916  	After           cty.Value
  6917  	Schema          *configschema.Block
  6918  	RequiredReplace cty.PathSet
  6919  	ExpectedOutput  string
  6920  	PrevRunAddr     addrs.AbsResourceInstance
  6921  }
  6922  
  6923  func runTestCases(t *testing.T, testCases map[string]testCase) {
  6924  	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
  6925  
  6926  	for name, tc := range testCases {
  6927  		t.Run(name, func(t *testing.T) {
  6928  			ty := tc.Schema.ImpliedType()
  6929  
  6930  			beforeVal := tc.Before
  6931  			switch { // Some fixups to make the test cases a little easier to write
  6932  			case beforeVal.IsNull():
  6933  				beforeVal = cty.NullVal(ty) // allow mistyped nulls
  6934  			case !beforeVal.IsKnown():
  6935  				beforeVal = cty.UnknownVal(ty) // allow mistyped unknowns
  6936  			}
  6937  
  6938  			afterVal := tc.After
  6939  			switch { // Some fixups to make the test cases a little easier to write
  6940  			case afterVal.IsNull():
  6941  				afterVal = cty.NullVal(ty) // allow mistyped nulls
  6942  			case !afterVal.IsKnown():
  6943  				afterVal = cty.UnknownVal(ty) // allow mistyped unknowns
  6944  			}
  6945  
  6946  			addr := addrs.Resource{
  6947  				Mode: tc.Mode,
  6948  				Type: "test_instance",
  6949  				Name: "example",
  6950  			}.Instance(tc.InstanceKey).Absolute(tc.ModuleInst)
  6951  
  6952  			prevRunAddr := tc.PrevRunAddr
  6953  			// If no previous run address is given, reuse the current address
  6954  			// to make initialization easier
  6955  			if prevRunAddr.Resource.Resource.Type == "" {
  6956  				prevRunAddr = addr
  6957  			}
  6958  
  6959  			beforeDynamicValue, err := plans.NewDynamicValue(beforeVal, ty)
  6960  			if err != nil {
  6961  				t.Fatalf("failed to create dynamic before value: " + err.Error())
  6962  			}
  6963  
  6964  			afterDynamicValue, err := plans.NewDynamicValue(afterVal, ty)
  6965  			if err != nil {
  6966  				t.Fatalf("failed to create dynamic after value: " + err.Error())
  6967  			}
  6968  
  6969  			src := &plans.ResourceInstanceChangeSrc{
  6970  				ChangeSrc: plans.ChangeSrc{
  6971  					Action:         tc.Action,
  6972  					Before:         beforeDynamicValue,
  6973  					BeforeValMarks: tc.BeforeValMarks,
  6974  					After:          afterDynamicValue,
  6975  					AfterValMarks:  tc.AfterValMarks,
  6976  				},
  6977  
  6978  				Addr:        addr,
  6979  				PrevRunAddr: prevRunAddr,
  6980  				DeposedKey:  tc.DeposedKey,
  6981  				ProviderAddr: addrs.AbsProviderConfig{
  6982  					Provider: addrs.NewDefaultProvider("test"),
  6983  					Module:   addrs.RootModule,
  6984  				},
  6985  				ActionReason:    tc.ActionReason,
  6986  				RequiredReplace: tc.RequiredReplace,
  6987  			}
  6988  
  6989  			tfschemas := &terraform.Schemas{
  6990  				Providers: map[addrs.Provider]providers.ProviderSchema{
  6991  					src.ProviderAddr.Provider: {
  6992  						ResourceTypes: map[string]providers.Schema{
  6993  							src.Addr.Resource.Resource.Type: {
  6994  								Block: tc.Schema,
  6995  							},
  6996  						},
  6997  						DataSources: map[string]providers.Schema{
  6998  							src.Addr.Resource.Resource.Type: {
  6999  								Block: tc.Schema,
  7000  							},
  7001  						},
  7002  					},
  7003  				},
  7004  			}
  7005  			jsonchanges, err := jsonplan.MarshalResourceChanges([]*plans.ResourceInstanceChangeSrc{src}, tfschemas)
  7006  			if err != nil {
  7007  				t.Errorf("failed to marshal resource changes: " + err.Error())
  7008  				return
  7009  			}
  7010  
  7011  			jsonschemas := jsonprovider.MarshalForRenderer(tfschemas)
  7012  			change := structured.FromJsonChange(jsonchanges[0].Change, attribute_path.AlwaysMatcher())
  7013  			renderer := Renderer{Colorize: color}
  7014  			diff := diff{
  7015  				change: jsonchanges[0],
  7016  				diff:   differ.ComputeDiffForBlock(change, jsonschemas[jsonchanges[0].ProviderName].ResourceSchemas[jsonchanges[0].Type].Block),
  7017  			}
  7018  			output, _ := renderHumanDiff(renderer, diff, proposedChange)
  7019  			if diff := cmp.Diff(output, tc.ExpectedOutput); diff != "" {
  7020  				t.Errorf("wrong output\nexpected:\n%s\nactual:\n%s\ndiff:\n%s\n", tc.ExpectedOutput, output, diff)
  7021  			}
  7022  		})
  7023  	}
  7024  }
  7025  
  7026  func TestOutputChanges(t *testing.T) {
  7027  	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
  7028  
  7029  	testCases := map[string]struct {
  7030  		changes []*plans.OutputChangeSrc
  7031  		output  string
  7032  	}{
  7033  		"new output value": {
  7034  			[]*plans.OutputChangeSrc{
  7035  				outputChange(
  7036  					"foo",
  7037  					cty.NullVal(cty.DynamicPseudoType),
  7038  					cty.StringVal("bar"),
  7039  					false,
  7040  				),
  7041  			},
  7042  			`  + foo = "bar"`,
  7043  		},
  7044  		"removed output": {
  7045  			[]*plans.OutputChangeSrc{
  7046  				outputChange(
  7047  					"foo",
  7048  					cty.StringVal("bar"),
  7049  					cty.NullVal(cty.DynamicPseudoType),
  7050  					false,
  7051  				),
  7052  			},
  7053  			`  - foo = "bar" -> null`,
  7054  		},
  7055  		"single string change": {
  7056  			[]*plans.OutputChangeSrc{
  7057  				outputChange(
  7058  					"foo",
  7059  					cty.StringVal("bar"),
  7060  					cty.StringVal("baz"),
  7061  					false,
  7062  				),
  7063  			},
  7064  			`  ~ foo = "bar" -> "baz"`,
  7065  		},
  7066  		"element added to list": {
  7067  			[]*plans.OutputChangeSrc{
  7068  				outputChange(
  7069  					"foo",
  7070  					cty.ListVal([]cty.Value{
  7071  						cty.StringVal("alpha"),
  7072  						cty.StringVal("beta"),
  7073  						cty.StringVal("delta"),
  7074  						cty.StringVal("epsilon"),
  7075  					}),
  7076  					cty.ListVal([]cty.Value{
  7077  						cty.StringVal("alpha"),
  7078  						cty.StringVal("beta"),
  7079  						cty.StringVal("gamma"),
  7080  						cty.StringVal("delta"),
  7081  						cty.StringVal("epsilon"),
  7082  					}),
  7083  					false,
  7084  				),
  7085  			},
  7086  			`  ~ foo = [
  7087          # (1 unchanged element hidden)
  7088          "beta",
  7089        + "gamma",
  7090          "delta",
  7091          # (1 unchanged element hidden)
  7092      ]`,
  7093  		},
  7094  		"multiple outputs changed, one sensitive": {
  7095  			[]*plans.OutputChangeSrc{
  7096  				outputChange(
  7097  					"a",
  7098  					cty.NumberIntVal(1),
  7099  					cty.NumberIntVal(2),
  7100  					false,
  7101  				),
  7102  				outputChange(
  7103  					"b",
  7104  					cty.StringVal("hunter2"),
  7105  					cty.StringVal("correct-horse-battery-staple"),
  7106  					true,
  7107  				),
  7108  				outputChange(
  7109  					"c",
  7110  					cty.BoolVal(false),
  7111  					cty.BoolVal(true),
  7112  					false,
  7113  				),
  7114  			},
  7115  			`  ~ a = 1 -> 2
  7116    ~ b = (sensitive value)
  7117    ~ c = false -> true`,
  7118  		},
  7119  	}
  7120  
  7121  	for name, tc := range testCases {
  7122  		t.Run(name, func(t *testing.T) {
  7123  			changes := &plans.Changes{
  7124  				Outputs: tc.changes,
  7125  			}
  7126  
  7127  			outputs, err := jsonplan.MarshalOutputChanges(changes)
  7128  			if err != nil {
  7129  				t.Fatalf("failed to marshal output changes")
  7130  			}
  7131  
  7132  			renderer := Renderer{Colorize: color}
  7133  			diffs := precomputeDiffs(Plan{
  7134  				OutputChanges: outputs,
  7135  			}, plans.NormalMode)
  7136  
  7137  			output := renderHumanDiffOutputs(renderer, diffs.outputs)
  7138  			if output != tc.output {
  7139  				t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.output)
  7140  			}
  7141  		})
  7142  	}
  7143  }
  7144  
  7145  func outputChange(name string, before, after cty.Value, sensitive bool) *plans.OutputChangeSrc {
  7146  	addr := addrs.AbsOutputValue{
  7147  		OutputValue: addrs.OutputValue{Name: name},
  7148  	}
  7149  
  7150  	change := &plans.OutputChange{
  7151  		Addr: addr, Change: plans.Change{
  7152  			Before: before,
  7153  			After:  after,
  7154  		},
  7155  		Sensitive: sensitive,
  7156  	}
  7157  
  7158  	changeSrc, err := change.Encode()
  7159  	if err != nil {
  7160  		panic(fmt.Sprintf("failed to encode change for %s: %s", addr, err))
  7161  	}
  7162  
  7163  	return changeSrc
  7164  }
  7165  
  7166  // A basic test schema using a configurable NestingMode for one (NestedType) attribute and one block
  7167  func testSchema(nesting configschema.NestingMode) *configschema.Block {
  7168  	var diskKey = "disks"
  7169  	if nesting == configschema.NestingSingle {
  7170  		diskKey = "disk"
  7171  	}
  7172  
  7173  	return &configschema.Block{
  7174  		Attributes: map[string]*configschema.Attribute{
  7175  			"id":  {Type: cty.String, Optional: true, Computed: true},
  7176  			"ami": {Type: cty.String, Optional: true},
  7177  			diskKey: {
  7178  				NestedType: &configschema.Object{
  7179  					Attributes: map[string]*configschema.Attribute{
  7180  						"mount_point": {Type: cty.String, Optional: true},
  7181  						"size":        {Type: cty.String, Optional: true},
  7182  					},
  7183  					Nesting: nesting,
  7184  				},
  7185  			},
  7186  		},
  7187  		BlockTypes: map[string]*configschema.NestedBlock{
  7188  			"root_block_device": {
  7189  				Block: configschema.Block{
  7190  					Attributes: map[string]*configschema.Attribute{
  7191  						"volume_type": {
  7192  							Type:     cty.String,
  7193  							Optional: true,
  7194  							Computed: true,
  7195  						},
  7196  					},
  7197  				},
  7198  				Nesting: nesting,
  7199  			},
  7200  		},
  7201  	}
  7202  }
  7203  
  7204  // A basic test schema using a configurable NestingMode for one (NestedType)
  7205  // attribute marked sensitive.
  7206  func testSchemaSensitive(nesting configschema.NestingMode) *configschema.Block {
  7207  	return &configschema.Block{
  7208  		Attributes: map[string]*configschema.Attribute{
  7209  			"id":  {Type: cty.String, Optional: true, Computed: true},
  7210  			"ami": {Type: cty.String, Optional: true},
  7211  			"disks": {
  7212  				Sensitive: true,
  7213  				NestedType: &configschema.Object{
  7214  					Attributes: map[string]*configschema.Attribute{
  7215  						"mount_point": {Type: cty.String, Optional: true},
  7216  						"size":        {Type: cty.String, Optional: true},
  7217  					},
  7218  					Nesting: nesting,
  7219  				},
  7220  			},
  7221  		},
  7222  	}
  7223  }
  7224  
  7225  func testSchemaMultipleBlocks(nesting configschema.NestingMode) *configschema.Block {
  7226  	return &configschema.Block{
  7227  		Attributes: map[string]*configschema.Attribute{
  7228  			"id":  {Type: cty.String, Optional: true, Computed: true},
  7229  			"ami": {Type: cty.String, Optional: true},
  7230  			"disks": {
  7231  				NestedType: &configschema.Object{
  7232  					Attributes: map[string]*configschema.Attribute{
  7233  						"mount_point": {Type: cty.String, Optional: true},
  7234  						"size":        {Type: cty.String, Optional: true},
  7235  					},
  7236  					Nesting: nesting,
  7237  				},
  7238  			},
  7239  		},
  7240  		BlockTypes: map[string]*configschema.NestedBlock{
  7241  			"root_block_device": {
  7242  				Block: configschema.Block{
  7243  					Attributes: map[string]*configschema.Attribute{
  7244  						"volume_type": {
  7245  							Type:     cty.String,
  7246  							Optional: true,
  7247  							Computed: true,
  7248  						},
  7249  					},
  7250  				},
  7251  				Nesting: nesting,
  7252  			},
  7253  			"leaf_block_device": {
  7254  				Block: configschema.Block{
  7255  					Attributes: map[string]*configschema.Attribute{
  7256  						"volume_type": {
  7257  							Type:     cty.String,
  7258  							Optional: true,
  7259  							Computed: true,
  7260  						},
  7261  					},
  7262  				},
  7263  				Nesting: nesting,
  7264  			},
  7265  		},
  7266  	}
  7267  }
  7268  
  7269  // similar to testSchema with the addition of a "new_field" block
  7270  func testSchemaPlus(nesting configschema.NestingMode) *configschema.Block {
  7271  	var diskKey = "disks"
  7272  	if nesting == configschema.NestingSingle {
  7273  		diskKey = "disk"
  7274  	}
  7275  
  7276  	return &configschema.Block{
  7277  		Attributes: map[string]*configschema.Attribute{
  7278  			"id":  {Type: cty.String, Optional: true, Computed: true},
  7279  			"ami": {Type: cty.String, Optional: true},
  7280  			diskKey: {
  7281  				NestedType: &configschema.Object{
  7282  					Attributes: map[string]*configschema.Attribute{
  7283  						"mount_point": {Type: cty.String, Optional: true},
  7284  						"size":        {Type: cty.String, Optional: true},
  7285  					},
  7286  					Nesting: nesting,
  7287  				},
  7288  			},
  7289  		},
  7290  		BlockTypes: map[string]*configschema.NestedBlock{
  7291  			"root_block_device": {
  7292  				Block: configschema.Block{
  7293  					Attributes: map[string]*configschema.Attribute{
  7294  						"volume_type": {
  7295  							Type:     cty.String,
  7296  							Optional: true,
  7297  							Computed: true,
  7298  						},
  7299  						"new_field": {
  7300  							Type:     cty.String,
  7301  							Optional: true,
  7302  							Computed: true,
  7303  						},
  7304  					},
  7305  				},
  7306  				Nesting: nesting,
  7307  			},
  7308  		},
  7309  	}
  7310  }
  7311  
  7312  func marshalJson(t *testing.T, data interface{}) json.RawMessage {
  7313  	result, err := json.Marshal(data)
  7314  	if err != nil {
  7315  		t.Fatalf("failed to marshal json: %v", err)
  7316  	}
  7317  	return result
  7318  }