github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/command/format/diff_test.go (about)

     1  package format
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  
     7  	"github.com/google/go-cmp/cmp"
     8  	"github.com/cycloidio/terraform/addrs"
     9  	"github.com/cycloidio/terraform/configs/configschema"
    10  	"github.com/cycloidio/terraform/lang/marks"
    11  	"github.com/cycloidio/terraform/plans"
    12  	"github.com/cycloidio/terraform/states"
    13  	"github.com/mitchellh/colorstring"
    14  	"github.com/zclconf/go-cty/cty"
    15  )
    16  
    17  func TestResourceChange_primitiveTypes(t *testing.T) {
    18  	testCases := map[string]testCase{
    19  		"creation": {
    20  			Action: plans.Create,
    21  			Mode:   addrs.ManagedResourceMode,
    22  			Before: cty.NullVal(cty.EmptyObject),
    23  			After: cty.ObjectVal(map[string]cty.Value{
    24  				"id": cty.UnknownVal(cty.String),
    25  			}),
    26  			Schema: &configschema.Block{
    27  				Attributes: map[string]*configschema.Attribute{
    28  					"id": {Type: cty.String, Computed: true},
    29  				},
    30  			},
    31  			RequiredReplace: cty.NewPathSet(),
    32  			ExpectedOutput: `  # test_instance.example will be created
    33    + resource "test_instance" "example" {
    34        + id = (known after apply)
    35      }
    36  `,
    37  		},
    38  		"creation (null string)": {
    39  			Action: plans.Create,
    40  			Mode:   addrs.ManagedResourceMode,
    41  			Before: cty.NullVal(cty.EmptyObject),
    42  			After: cty.ObjectVal(map[string]cty.Value{
    43  				"string": cty.StringVal("null"),
    44  			}),
    45  			Schema: &configschema.Block{
    46  				Attributes: map[string]*configschema.Attribute{
    47  					"string": {Type: cty.String, Optional: true},
    48  				},
    49  			},
    50  			RequiredReplace: cty.NewPathSet(),
    51  			ExpectedOutput: `  # test_instance.example will be created
    52    + resource "test_instance" "example" {
    53        + string = "null"
    54      }
    55  `,
    56  		},
    57  		"creation (null string with extra whitespace)": {
    58  			Action: plans.Create,
    59  			Mode:   addrs.ManagedResourceMode,
    60  			Before: cty.NullVal(cty.EmptyObject),
    61  			After: cty.ObjectVal(map[string]cty.Value{
    62  				"string": cty.StringVal("null "),
    63  			}),
    64  			Schema: &configschema.Block{
    65  				Attributes: map[string]*configschema.Attribute{
    66  					"string": {Type: cty.String, Optional: true},
    67  				},
    68  			},
    69  			RequiredReplace: cty.NewPathSet(),
    70  			ExpectedOutput: `  # test_instance.example will be created
    71    + resource "test_instance" "example" {
    72        + string = "null "
    73      }
    74  `,
    75  		},
    76  		"creation (object with quoted keys)": {
    77  			Action: plans.Create,
    78  			Mode:   addrs.ManagedResourceMode,
    79  			Before: cty.NullVal(cty.EmptyObject),
    80  			After: cty.ObjectVal(map[string]cty.Value{
    81  				"object": cty.ObjectVal(map[string]cty.Value{
    82  					"unquoted":   cty.StringVal("value"),
    83  					"quoted:key": cty.StringVal("some-value"),
    84  				}),
    85  			}),
    86  			Schema: &configschema.Block{
    87  				Attributes: map[string]*configschema.Attribute{
    88  					"object": {Type: cty.Object(map[string]cty.Type{
    89  						"unquoted":   cty.String,
    90  						"quoted:key": cty.String,
    91  					}), Optional: true},
    92  				},
    93  			},
    94  			RequiredReplace: cty.NewPathSet(),
    95  			ExpectedOutput: `  # test_instance.example will be created
    96    + resource "test_instance" "example" {
    97        + object = {
    98            + "quoted:key" = "some-value"
    99            + unquoted     = "value"
   100          }
   101      }
   102  `,
   103  		},
   104  		"deletion": {
   105  			Action: plans.Delete,
   106  			Mode:   addrs.ManagedResourceMode,
   107  			Before: cty.ObjectVal(map[string]cty.Value{
   108  				"id": cty.StringVal("i-02ae66f368e8518a9"),
   109  			}),
   110  			After: cty.NullVal(cty.EmptyObject),
   111  			Schema: &configschema.Block{
   112  				Attributes: map[string]*configschema.Attribute{
   113  					"id": {Type: cty.String, Computed: true},
   114  				},
   115  			},
   116  			RequiredReplace: cty.NewPathSet(),
   117  			ExpectedOutput: `  # test_instance.example will be destroyed
   118    - resource "test_instance" "example" {
   119        - id = "i-02ae66f368e8518a9" -> null
   120      }
   121  `,
   122  		},
   123  		"deletion of deposed object": {
   124  			Action:     plans.Delete,
   125  			Mode:       addrs.ManagedResourceMode,
   126  			DeposedKey: states.DeposedKey("byebye"),
   127  			Before: cty.ObjectVal(map[string]cty.Value{
   128  				"id": cty.StringVal("i-02ae66f368e8518a9"),
   129  			}),
   130  			After: cty.NullVal(cty.EmptyObject),
   131  			Schema: &configschema.Block{
   132  				Attributes: map[string]*configschema.Attribute{
   133  					"id": {Type: cty.String, Computed: true},
   134  				},
   135  			},
   136  			RequiredReplace: cty.NewPathSet(),
   137  			ExpectedOutput: `  # test_instance.example (deposed object byebye) will be destroyed
   138    # (left over from a partially-failed replacement of this instance)
   139    - resource "test_instance" "example" {
   140        - id = "i-02ae66f368e8518a9" -> null
   141      }
   142  `,
   143  		},
   144  		"deletion (empty string)": {
   145  			Action: plans.Delete,
   146  			Mode:   addrs.ManagedResourceMode,
   147  			Before: cty.ObjectVal(map[string]cty.Value{
   148  				"id":                 cty.StringVal("i-02ae66f368e8518a9"),
   149  				"intentionally_long": cty.StringVal(""),
   150  			}),
   151  			After: cty.NullVal(cty.EmptyObject),
   152  			Schema: &configschema.Block{
   153  				Attributes: map[string]*configschema.Attribute{
   154  					"id":                 {Type: cty.String, Computed: true},
   155  					"intentionally_long": {Type: cty.String, Optional: true},
   156  				},
   157  			},
   158  			RequiredReplace: cty.NewPathSet(),
   159  			ExpectedOutput: `  # test_instance.example will be destroyed
   160    - resource "test_instance" "example" {
   161        - id = "i-02ae66f368e8518a9" -> null
   162      }
   163  `,
   164  		},
   165  		"string in-place update": {
   166  			Action: plans.Update,
   167  			Mode:   addrs.ManagedResourceMode,
   168  			Before: cty.ObjectVal(map[string]cty.Value{
   169  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   170  				"ami": cty.StringVal("ami-BEFORE"),
   171  			}),
   172  			After: cty.ObjectVal(map[string]cty.Value{
   173  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   174  				"ami": cty.StringVal("ami-AFTER"),
   175  			}),
   176  			Schema: &configschema.Block{
   177  				Attributes: map[string]*configschema.Attribute{
   178  					"id":  {Type: cty.String, Optional: true, Computed: true},
   179  					"ami": {Type: cty.String, Optional: true},
   180  				},
   181  			},
   182  			RequiredReplace: cty.NewPathSet(),
   183  			ExpectedOutput: `  # test_instance.example will be updated in-place
   184    ~ resource "test_instance" "example" {
   185        ~ ami = "ami-BEFORE" -> "ami-AFTER"
   186          id  = "i-02ae66f368e8518a9"
   187      }
   188  `,
   189  		},
   190  		"update with quoted key": {
   191  			Action: plans.Update,
   192  			Mode:   addrs.ManagedResourceMode,
   193  			Before: cty.ObjectVal(map[string]cty.Value{
   194  				"id":       cty.StringVal("i-02ae66f368e8518a9"),
   195  				"saml:aud": cty.StringVal("https://example.com/saml"),
   196  				"zeta":     cty.StringVal("alpha"),
   197  			}),
   198  			After: cty.ObjectVal(map[string]cty.Value{
   199  				"id":       cty.StringVal("i-02ae66f368e8518a9"),
   200  				"saml:aud": cty.StringVal("https://saml.example.com"),
   201  				"zeta":     cty.StringVal("alpha"),
   202  			}),
   203  			Schema: &configschema.Block{
   204  				Attributes: map[string]*configschema.Attribute{
   205  					"id":       {Type: cty.String, Optional: true, Computed: true},
   206  					"saml:aud": {Type: cty.String, Optional: true},
   207  					"zeta":     {Type: cty.String, Optional: true},
   208  				},
   209  			},
   210  			RequiredReplace: cty.NewPathSet(),
   211  			ExpectedOutput: `  # test_instance.example will be updated in-place
   212    ~ resource "test_instance" "example" {
   213          id         = "i-02ae66f368e8518a9"
   214        ~ "saml:aud" = "https://example.com/saml" -> "https://saml.example.com"
   215          # (1 unchanged attribute hidden)
   216      }
   217  `,
   218  		},
   219  		"string force-new update": {
   220  			Action:       plans.DeleteThenCreate,
   221  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
   222  			Mode:         addrs.ManagedResourceMode,
   223  			Before: cty.ObjectVal(map[string]cty.Value{
   224  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   225  				"ami": cty.StringVal("ami-BEFORE"),
   226  			}),
   227  			After: cty.ObjectVal(map[string]cty.Value{
   228  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   229  				"ami": cty.StringVal("ami-AFTER"),
   230  			}),
   231  			Schema: &configschema.Block{
   232  				Attributes: map[string]*configschema.Attribute{
   233  					"id":  {Type: cty.String, Optional: true, Computed: true},
   234  					"ami": {Type: cty.String, Optional: true},
   235  				},
   236  			},
   237  			RequiredReplace: cty.NewPathSet(cty.Path{
   238  				cty.GetAttrStep{Name: "ami"},
   239  			}),
   240  			ExpectedOutput: `  # test_instance.example must be replaced
   241  -/+ resource "test_instance" "example" {
   242        ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement
   243          id  = "i-02ae66f368e8518a9"
   244      }
   245  `,
   246  		},
   247  		"string in-place update (null values)": {
   248  			Action: plans.Update,
   249  			Mode:   addrs.ManagedResourceMode,
   250  			Before: cty.ObjectVal(map[string]cty.Value{
   251  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
   252  				"ami":       cty.StringVal("ami-BEFORE"),
   253  				"unchanged": cty.NullVal(cty.String),
   254  			}),
   255  			After: cty.ObjectVal(map[string]cty.Value{
   256  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
   257  				"ami":       cty.StringVal("ami-AFTER"),
   258  				"unchanged": cty.NullVal(cty.String),
   259  			}),
   260  			Schema: &configschema.Block{
   261  				Attributes: map[string]*configschema.Attribute{
   262  					"id":        {Type: cty.String, Optional: true, Computed: true},
   263  					"ami":       {Type: cty.String, Optional: true},
   264  					"unchanged": {Type: cty.String, Optional: true},
   265  				},
   266  			},
   267  			RequiredReplace: cty.NewPathSet(),
   268  			ExpectedOutput: `  # test_instance.example will be updated in-place
   269    ~ resource "test_instance" "example" {
   270        ~ ami = "ami-BEFORE" -> "ami-AFTER"
   271          id  = "i-02ae66f368e8518a9"
   272      }
   273  `,
   274  		},
   275  		"in-place update of multi-line string field": {
   276  			Action: plans.Update,
   277  			Mode:   addrs.ManagedResourceMode,
   278  			Before: cty.ObjectVal(map[string]cty.Value{
   279  				"id": cty.StringVal("i-02ae66f368e8518a9"),
   280  				"more_lines": cty.StringVal(`original
   281  long
   282  multi-line
   283  string
   284  field
   285  `),
   286  			}),
   287  			After: cty.ObjectVal(map[string]cty.Value{
   288  				"id": cty.UnknownVal(cty.String),
   289  				"more_lines": cty.StringVal(`original
   290  extremely long
   291  multi-line
   292  string
   293  field
   294  `),
   295  			}),
   296  			Schema: &configschema.Block{
   297  				Attributes: map[string]*configschema.Attribute{
   298  					"id":         {Type: cty.String, Optional: true, Computed: true},
   299  					"more_lines": {Type: cty.String, Optional: true},
   300  				},
   301  			},
   302  			RequiredReplace: cty.NewPathSet(),
   303  			ExpectedOutput: `  # test_instance.example will be updated in-place
   304    ~ resource "test_instance" "example" {
   305        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   306        ~ more_lines = <<-EOT
   307              original
   308            - long
   309            + extremely long
   310              multi-line
   311              string
   312              field
   313          EOT
   314      }
   315  `,
   316  		},
   317  		"addition of multi-line string field": {
   318  			Action: plans.Update,
   319  			Mode:   addrs.ManagedResourceMode,
   320  			Before: cty.ObjectVal(map[string]cty.Value{
   321  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   322  				"more_lines": cty.NullVal(cty.String),
   323  			}),
   324  			After: cty.ObjectVal(map[string]cty.Value{
   325  				"id": cty.UnknownVal(cty.String),
   326  				"more_lines": cty.StringVal(`original
   327  new line
   328  `),
   329  			}),
   330  			Schema: &configschema.Block{
   331  				Attributes: map[string]*configschema.Attribute{
   332  					"id":         {Type: cty.String, Optional: true, Computed: true},
   333  					"more_lines": {Type: cty.String, Optional: true},
   334  				},
   335  			},
   336  			RequiredReplace: cty.NewPathSet(),
   337  			ExpectedOutput: `  # test_instance.example will be updated in-place
   338    ~ resource "test_instance" "example" {
   339        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   340        + more_lines = <<-EOT
   341              original
   342              new line
   343          EOT
   344      }
   345  `,
   346  		},
   347  		"force-new update of multi-line string field": {
   348  			Action: plans.DeleteThenCreate,
   349  			Mode:   addrs.ManagedResourceMode,
   350  			Before: cty.ObjectVal(map[string]cty.Value{
   351  				"id": cty.StringVal("i-02ae66f368e8518a9"),
   352  				"more_lines": cty.StringVal(`original
   353  `),
   354  			}),
   355  			After: cty.ObjectVal(map[string]cty.Value{
   356  				"id": cty.UnknownVal(cty.String),
   357  				"more_lines": cty.StringVal(`original
   358  new line
   359  `),
   360  			}),
   361  			Schema: &configschema.Block{
   362  				Attributes: map[string]*configschema.Attribute{
   363  					"id":         {Type: cty.String, Optional: true, Computed: true},
   364  					"more_lines": {Type: cty.String, Optional: true},
   365  				},
   366  			},
   367  			RequiredReplace: cty.NewPathSet(cty.Path{
   368  				cty.GetAttrStep{Name: "more_lines"},
   369  			}),
   370  			ExpectedOutput: `  # test_instance.example must be replaced
   371  -/+ resource "test_instance" "example" {
   372        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   373        ~ more_lines = <<-EOT # forces replacement
   374              original
   375            + new line
   376          EOT
   377      }
   378  `,
   379  		},
   380  
   381  		// Sensitive
   382  
   383  		"creation with sensitive field": {
   384  			Action: plans.Create,
   385  			Mode:   addrs.ManagedResourceMode,
   386  			Before: cty.NullVal(cty.EmptyObject),
   387  			After: cty.ObjectVal(map[string]cty.Value{
   388  				"id":       cty.UnknownVal(cty.String),
   389  				"password": cty.StringVal("top-secret"),
   390  				"conn_info": cty.ObjectVal(map[string]cty.Value{
   391  					"user":     cty.StringVal("not-secret"),
   392  					"password": cty.StringVal("top-secret"),
   393  				}),
   394  			}),
   395  			Schema: &configschema.Block{
   396  				Attributes: map[string]*configschema.Attribute{
   397  					"id":       {Type: cty.String, Computed: true},
   398  					"password": {Type: cty.String, Optional: true, Sensitive: true},
   399  					"conn_info": {
   400  						NestedType: &configschema.Object{
   401  							Nesting: configschema.NestingSingle,
   402  							Attributes: map[string]*configschema.Attribute{
   403  								"user":     {Type: cty.String, Optional: true},
   404  								"password": {Type: cty.String, Optional: true, Sensitive: true},
   405  							},
   406  						},
   407  					},
   408  				},
   409  			},
   410  			RequiredReplace: cty.NewPathSet(),
   411  			ExpectedOutput: `  # test_instance.example will be created
   412    + resource "test_instance" "example" {
   413        + conn_info = {
   414            + password = (sensitive value)
   415            + user     = "not-secret"
   416          }
   417        + id        = (known after apply)
   418        + password  = (sensitive value)
   419      }
   420  `,
   421  		},
   422  		"update with equal sensitive field": {
   423  			Action: plans.Update,
   424  			Mode:   addrs.ManagedResourceMode,
   425  			Before: cty.ObjectVal(map[string]cty.Value{
   426  				"id":       cty.StringVal("blah"),
   427  				"str":      cty.StringVal("before"),
   428  				"password": cty.StringVal("top-secret"),
   429  			}),
   430  			After: cty.ObjectVal(map[string]cty.Value{
   431  				"id":       cty.UnknownVal(cty.String),
   432  				"str":      cty.StringVal("after"),
   433  				"password": cty.StringVal("top-secret"),
   434  			}),
   435  			Schema: &configschema.Block{
   436  				Attributes: map[string]*configschema.Attribute{
   437  					"id":       {Type: cty.String, Computed: true},
   438  					"str":      {Type: cty.String, Optional: true},
   439  					"password": {Type: cty.String, Optional: true, Sensitive: true},
   440  				},
   441  			},
   442  			RequiredReplace: cty.NewPathSet(),
   443  			ExpectedOutput: `  # test_instance.example will be updated in-place
   444    ~ resource "test_instance" "example" {
   445        ~ id       = "blah" -> (known after apply)
   446        ~ str      = "before" -> "after"
   447          # (1 unchanged attribute hidden)
   448      }
   449  `,
   450  		},
   451  
   452  		// tainted objects
   453  		"replace tainted resource": {
   454  			Action:       plans.DeleteThenCreate,
   455  			ActionReason: plans.ResourceInstanceReplaceBecauseTainted,
   456  			Mode:         addrs.ManagedResourceMode,
   457  			Before: cty.ObjectVal(map[string]cty.Value{
   458  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   459  				"ami": cty.StringVal("ami-BEFORE"),
   460  			}),
   461  			After: cty.ObjectVal(map[string]cty.Value{
   462  				"id":  cty.UnknownVal(cty.String),
   463  				"ami": cty.StringVal("ami-AFTER"),
   464  			}),
   465  			Schema: &configschema.Block{
   466  				Attributes: map[string]*configschema.Attribute{
   467  					"id":  {Type: cty.String, Optional: true, Computed: true},
   468  					"ami": {Type: cty.String, Optional: true},
   469  				},
   470  			},
   471  			RequiredReplace: cty.NewPathSet(cty.Path{
   472  				cty.GetAttrStep{Name: "ami"},
   473  			}),
   474  			ExpectedOutput: `  # test_instance.example is tainted, so must be replaced
   475  -/+ resource "test_instance" "example" {
   476        ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement
   477        ~ id  = "i-02ae66f368e8518a9" -> (known after apply)
   478      }
   479  `,
   480  		},
   481  		"force replacement with empty before value": {
   482  			Action:       plans.DeleteThenCreate,
   483  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
   484  			Mode:         addrs.ManagedResourceMode,
   485  			Before: cty.ObjectVal(map[string]cty.Value{
   486  				"name":   cty.StringVal("name"),
   487  				"forced": cty.NullVal(cty.String),
   488  			}),
   489  			After: cty.ObjectVal(map[string]cty.Value{
   490  				"name":   cty.StringVal("name"),
   491  				"forced": cty.StringVal("example"),
   492  			}),
   493  			Schema: &configschema.Block{
   494  				Attributes: map[string]*configschema.Attribute{
   495  					"name":   {Type: cty.String, Optional: true},
   496  					"forced": {Type: cty.String, Optional: true},
   497  				},
   498  			},
   499  			RequiredReplace: cty.NewPathSet(cty.Path{
   500  				cty.GetAttrStep{Name: "forced"},
   501  			}),
   502  			ExpectedOutput: `  # test_instance.example must be replaced
   503  -/+ resource "test_instance" "example" {
   504        + forced = "example" # forces replacement
   505          name   = "name"
   506      }
   507  `,
   508  		},
   509  		"force replacement with empty before value legacy": {
   510  			Action:       plans.DeleteThenCreate,
   511  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
   512  			Mode:         addrs.ManagedResourceMode,
   513  			Before: cty.ObjectVal(map[string]cty.Value{
   514  				"name":   cty.StringVal("name"),
   515  				"forced": cty.StringVal(""),
   516  			}),
   517  			After: cty.ObjectVal(map[string]cty.Value{
   518  				"name":   cty.StringVal("name"),
   519  				"forced": cty.StringVal("example"),
   520  			}),
   521  			Schema: &configschema.Block{
   522  				Attributes: map[string]*configschema.Attribute{
   523  					"name":   {Type: cty.String, Optional: true},
   524  					"forced": {Type: cty.String, Optional: true},
   525  				},
   526  			},
   527  			RequiredReplace: cty.NewPathSet(cty.Path{
   528  				cty.GetAttrStep{Name: "forced"},
   529  			}),
   530  			ExpectedOutput: `  # test_instance.example must be replaced
   531  -/+ resource "test_instance" "example" {
   532        + forced = "example" # forces replacement
   533          name   = "name"
   534      }
   535  `,
   536  		},
   537  		"show all identifying attributes even if unchanged": {
   538  			Action: plans.Update,
   539  			Mode:   addrs.ManagedResourceMode,
   540  			Before: cty.ObjectVal(map[string]cty.Value{
   541  				"id":   cty.StringVal("i-02ae66f368e8518a9"),
   542  				"ami":  cty.StringVal("ami-BEFORE"),
   543  				"bar":  cty.StringVal("bar"),
   544  				"foo":  cty.StringVal("foo"),
   545  				"name": cty.StringVal("alice"),
   546  				"tags": cty.MapVal(map[string]cty.Value{
   547  					"name": cty.StringVal("bob"),
   548  				}),
   549  			}),
   550  			After: cty.ObjectVal(map[string]cty.Value{
   551  				"id":   cty.StringVal("i-02ae66f368e8518a9"),
   552  				"ami":  cty.StringVal("ami-AFTER"),
   553  				"bar":  cty.StringVal("bar"),
   554  				"foo":  cty.StringVal("foo"),
   555  				"name": cty.StringVal("alice"),
   556  				"tags": cty.MapVal(map[string]cty.Value{
   557  					"name": cty.StringVal("bob"),
   558  				}),
   559  			}),
   560  			Schema: &configschema.Block{
   561  				Attributes: map[string]*configschema.Attribute{
   562  					"id":   {Type: cty.String, Optional: true, Computed: true},
   563  					"ami":  {Type: cty.String, Optional: true},
   564  					"bar":  {Type: cty.String, Optional: true},
   565  					"foo":  {Type: cty.String, Optional: true},
   566  					"name": {Type: cty.String, Optional: true},
   567  					"tags": {Type: cty.Map(cty.String), Optional: true},
   568  				},
   569  			},
   570  			RequiredReplace: cty.NewPathSet(),
   571  			ExpectedOutput: `  # test_instance.example will be updated in-place
   572    ~ resource "test_instance" "example" {
   573        ~ ami  = "ami-BEFORE" -> "ami-AFTER"
   574          id   = "i-02ae66f368e8518a9"
   575          name = "alice"
   576          tags = {
   577              "name" = "bob"
   578          }
   579          # (2 unchanged attributes hidden)
   580      }
   581  `,
   582  		},
   583  	}
   584  
   585  	runTestCases(t, testCases)
   586  }
   587  
   588  func TestResourceChange_JSON(t *testing.T) {
   589  	testCases := map[string]testCase{
   590  		"creation": {
   591  			Action: plans.Create,
   592  			Mode:   addrs.ManagedResourceMode,
   593  			Before: cty.NullVal(cty.EmptyObject),
   594  			After: cty.ObjectVal(map[string]cty.Value{
   595  				"id": cty.UnknownVal(cty.String),
   596  				"json_field": cty.StringVal(`{
   597  					"str": "value",
   598  					"list":["a","b", 234, true],
   599  					"obj": {"key": "val"}
   600  				}`),
   601  			}),
   602  			Schema: &configschema.Block{
   603  				Attributes: map[string]*configschema.Attribute{
   604  					"id":         {Type: cty.String, Optional: true, Computed: true},
   605  					"json_field": {Type: cty.String, Optional: true},
   606  				},
   607  			},
   608  			RequiredReplace: cty.NewPathSet(),
   609  			ExpectedOutput: `  # test_instance.example will be created
   610    + resource "test_instance" "example" {
   611        + id         = (known after apply)
   612        + json_field = jsonencode(
   613              {
   614                + list = [
   615                    + "a",
   616                    + "b",
   617                    + 234,
   618                    + true,
   619                  ]
   620                + obj  = {
   621                    + key = "val"
   622                  }
   623                + str  = "value"
   624              }
   625          )
   626      }
   627  `,
   628  		},
   629  		"in-place update of object": {
   630  			Action: plans.Update,
   631  			Mode:   addrs.ManagedResourceMode,
   632  			Before: cty.ObjectVal(map[string]cty.Value{
   633  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   634  				"json_field": cty.StringVal(`{"aaa": "value","ccc": 5}`),
   635  			}),
   636  			After: cty.ObjectVal(map[string]cty.Value{
   637  				"id":         cty.UnknownVal(cty.String),
   638  				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`),
   639  			}),
   640  			Schema: &configschema.Block{
   641  				Attributes: map[string]*configschema.Attribute{
   642  					"id":         {Type: cty.String, Optional: true, Computed: true},
   643  					"json_field": {Type: cty.String, Optional: true},
   644  				},
   645  			},
   646  			RequiredReplace: cty.NewPathSet(),
   647  			ExpectedOutput: `  # test_instance.example will be updated in-place
   648    ~ resource "test_instance" "example" {
   649        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   650        ~ json_field = jsonencode(
   651            ~ {
   652                + bbb = "new_value"
   653                - ccc = 5 -> null
   654                  # (1 unchanged element hidden)
   655              }
   656          )
   657      }
   658  `,
   659  		},
   660  		"in-place update of object with quoted keys": {
   661  			Action: plans.Update,
   662  			Mode:   addrs.ManagedResourceMode,
   663  			Before: cty.ObjectVal(map[string]cty.Value{
   664  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   665  				"json_field": cty.StringVal(`{"aaa": "value", "c:c": "old_value"}`),
   666  			}),
   667  			After: cty.ObjectVal(map[string]cty.Value{
   668  				"id":         cty.UnknownVal(cty.String),
   669  				"json_field": cty.StringVal(`{"aaa": "value", "b:bb": "new_value"}`),
   670  			}),
   671  			Schema: &configschema.Block{
   672  				Attributes: map[string]*configschema.Attribute{
   673  					"id":         {Type: cty.String, Optional: true, Computed: true},
   674  					"json_field": {Type: cty.String, Optional: true},
   675  				},
   676  			},
   677  			RequiredReplace: cty.NewPathSet(),
   678  			ExpectedOutput: `  # test_instance.example will be updated in-place
   679    ~ resource "test_instance" "example" {
   680        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   681        ~ json_field = jsonencode(
   682            ~ {
   683                + "b:bb" = "new_value"
   684                - "c:c"  = "old_value" -> null
   685                  # (1 unchanged element hidden)
   686              }
   687          )
   688      }
   689  `,
   690  		},
   691  		"in-place update (from empty tuple)": {
   692  			Action: plans.Update,
   693  			Mode:   addrs.ManagedResourceMode,
   694  			Before: cty.ObjectVal(map[string]cty.Value{
   695  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   696  				"json_field": cty.StringVal(`{"aaa": []}`),
   697  			}),
   698  			After: cty.ObjectVal(map[string]cty.Value{
   699  				"id":         cty.UnknownVal(cty.String),
   700  				"json_field": cty.StringVal(`{"aaa": ["value"]}`),
   701  			}),
   702  			Schema: &configschema.Block{
   703  				Attributes: map[string]*configschema.Attribute{
   704  					"id":         {Type: cty.String, Optional: true, Computed: true},
   705  					"json_field": {Type: cty.String, Optional: true},
   706  				},
   707  			},
   708  			RequiredReplace: cty.NewPathSet(),
   709  			ExpectedOutput: `  # test_instance.example will be updated in-place
   710    ~ resource "test_instance" "example" {
   711        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   712        ~ json_field = jsonencode(
   713            ~ {
   714                ~ aaa = [
   715                    + "value",
   716                  ]
   717              }
   718          )
   719      }
   720  `,
   721  		},
   722  		"in-place update (to empty tuple)": {
   723  			Action: plans.Update,
   724  			Mode:   addrs.ManagedResourceMode,
   725  			Before: cty.ObjectVal(map[string]cty.Value{
   726  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   727  				"json_field": cty.StringVal(`{"aaa": ["value"]}`),
   728  			}),
   729  			After: cty.ObjectVal(map[string]cty.Value{
   730  				"id":         cty.UnknownVal(cty.String),
   731  				"json_field": cty.StringVal(`{"aaa": []}`),
   732  			}),
   733  			Schema: &configschema.Block{
   734  				Attributes: map[string]*configschema.Attribute{
   735  					"id":         {Type: cty.String, Optional: true, Computed: true},
   736  					"json_field": {Type: cty.String, Optional: true},
   737  				},
   738  			},
   739  			RequiredReplace: cty.NewPathSet(),
   740  			ExpectedOutput: `  # test_instance.example will be updated in-place
   741    ~ resource "test_instance" "example" {
   742        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   743        ~ json_field = jsonencode(
   744            ~ {
   745                ~ aaa = [
   746                    - "value",
   747                  ]
   748              }
   749          )
   750      }
   751  `,
   752  		},
   753  		"in-place update (tuple of different types)": {
   754  			Action: plans.Update,
   755  			Mode:   addrs.ManagedResourceMode,
   756  			Before: cty.ObjectVal(map[string]cty.Value{
   757  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   758  				"json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`),
   759  			}),
   760  			After: cty.ObjectVal(map[string]cty.Value{
   761  				"id":         cty.UnknownVal(cty.String),
   762  				"json_field": cty.StringVal(`{"aaa": [42, {"foo":"baz"}, "value"]}`),
   763  			}),
   764  			Schema: &configschema.Block{
   765  				Attributes: map[string]*configschema.Attribute{
   766  					"id":         {Type: cty.String, Optional: true, Computed: true},
   767  					"json_field": {Type: cty.String, Optional: true},
   768  				},
   769  			},
   770  			RequiredReplace: cty.NewPathSet(),
   771  			ExpectedOutput: `  # test_instance.example will be updated in-place
   772    ~ resource "test_instance" "example" {
   773        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   774        ~ json_field = jsonencode(
   775            ~ {
   776                ~ aaa = [
   777                      42,
   778                    ~ {
   779                        ~ foo = "bar" -> "baz"
   780                      },
   781                      "value",
   782                  ]
   783              }
   784          )
   785      }
   786  `,
   787  		},
   788  		"force-new update": {
   789  			Action:       plans.DeleteThenCreate,
   790  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
   791  			Mode:         addrs.ManagedResourceMode,
   792  			Before: cty.ObjectVal(map[string]cty.Value{
   793  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   794  				"json_field": cty.StringVal(`{"aaa": "value"}`),
   795  			}),
   796  			After: cty.ObjectVal(map[string]cty.Value{
   797  				"id":         cty.UnknownVal(cty.String),
   798  				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`),
   799  			}),
   800  			Schema: &configschema.Block{
   801  				Attributes: map[string]*configschema.Attribute{
   802  					"id":         {Type: cty.String, Optional: true, Computed: true},
   803  					"json_field": {Type: cty.String, Optional: true},
   804  				},
   805  			},
   806  			RequiredReplace: cty.NewPathSet(cty.Path{
   807  				cty.GetAttrStep{Name: "json_field"},
   808  			}),
   809  			ExpectedOutput: `  # test_instance.example must be replaced
   810  -/+ resource "test_instance" "example" {
   811        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   812        ~ json_field = jsonencode(
   813            ~ {
   814                + bbb = "new_value"
   815                  # (1 unchanged element hidden)
   816              } # forces replacement
   817          )
   818      }
   819  `,
   820  		},
   821  		"in-place update (whitespace change)": {
   822  			Action: plans.Update,
   823  			Mode:   addrs.ManagedResourceMode,
   824  			Before: cty.ObjectVal(map[string]cty.Value{
   825  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   826  				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`),
   827  			}),
   828  			After: cty.ObjectVal(map[string]cty.Value{
   829  				"id": cty.UnknownVal(cty.String),
   830  				"json_field": cty.StringVal(`{"aaa":"value",
   831  					"bbb":"another"}`),
   832  			}),
   833  			Schema: &configschema.Block{
   834  				Attributes: map[string]*configschema.Attribute{
   835  					"id":         {Type: cty.String, Optional: true, Computed: true},
   836  					"json_field": {Type: cty.String, Optional: true},
   837  				},
   838  			},
   839  			RequiredReplace: cty.NewPathSet(),
   840  			ExpectedOutput: `  # test_instance.example will be updated in-place
   841    ~ resource "test_instance" "example" {
   842        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   843        ~ json_field = jsonencode( # whitespace changes
   844              {
   845                  aaa = "value"
   846                  bbb = "another"
   847              }
   848          )
   849      }
   850  `,
   851  		},
   852  		"force-new update (whitespace change)": {
   853  			Action:       plans.DeleteThenCreate,
   854  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
   855  			Mode:         addrs.ManagedResourceMode,
   856  			Before: cty.ObjectVal(map[string]cty.Value{
   857  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   858  				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`),
   859  			}),
   860  			After: cty.ObjectVal(map[string]cty.Value{
   861  				"id": cty.UnknownVal(cty.String),
   862  				"json_field": cty.StringVal(`{"aaa":"value",
   863  					"bbb":"another"}`),
   864  			}),
   865  			Schema: &configschema.Block{
   866  				Attributes: map[string]*configschema.Attribute{
   867  					"id":         {Type: cty.String, Optional: true, Computed: true},
   868  					"json_field": {Type: cty.String, Optional: true},
   869  				},
   870  			},
   871  			RequiredReplace: cty.NewPathSet(cty.Path{
   872  				cty.GetAttrStep{Name: "json_field"},
   873  			}),
   874  			ExpectedOutput: `  # test_instance.example must be replaced
   875  -/+ resource "test_instance" "example" {
   876        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   877        ~ json_field = jsonencode( # whitespace changes force replacement
   878              {
   879                  aaa = "value"
   880                  bbb = "another"
   881              }
   882          )
   883      }
   884  `,
   885  		},
   886  		"creation (empty)": {
   887  			Action: plans.Create,
   888  			Mode:   addrs.ManagedResourceMode,
   889  			Before: cty.NullVal(cty.EmptyObject),
   890  			After: cty.ObjectVal(map[string]cty.Value{
   891  				"id":         cty.UnknownVal(cty.String),
   892  				"json_field": cty.StringVal(`{}`),
   893  			}),
   894  			Schema: &configschema.Block{
   895  				Attributes: map[string]*configschema.Attribute{
   896  					"id":         {Type: cty.String, Optional: true, Computed: true},
   897  					"json_field": {Type: cty.String, Optional: true},
   898  				},
   899  			},
   900  			RequiredReplace: cty.NewPathSet(),
   901  			ExpectedOutput: `  # test_instance.example will be created
   902    + resource "test_instance" "example" {
   903        + id         = (known after apply)
   904        + json_field = jsonencode({})
   905      }
   906  `,
   907  		},
   908  		"JSON list item removal": {
   909  			Action: plans.Update,
   910  			Mode:   addrs.ManagedResourceMode,
   911  			Before: cty.ObjectVal(map[string]cty.Value{
   912  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   913  				"json_field": cty.StringVal(`["first","second","third"]`),
   914  			}),
   915  			After: cty.ObjectVal(map[string]cty.Value{
   916  				"id":         cty.UnknownVal(cty.String),
   917  				"json_field": cty.StringVal(`["first","second"]`),
   918  			}),
   919  			Schema: &configschema.Block{
   920  				Attributes: map[string]*configschema.Attribute{
   921  					"id":         {Type: cty.String, Optional: true, Computed: true},
   922  					"json_field": {Type: cty.String, Optional: true},
   923  				},
   924  			},
   925  			RequiredReplace: cty.NewPathSet(),
   926  			ExpectedOutput: `  # test_instance.example will be updated in-place
   927    ~ resource "test_instance" "example" {
   928        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   929        ~ json_field = jsonencode(
   930            ~ [
   931                  # (1 unchanged element hidden)
   932                  "second",
   933                - "third",
   934              ]
   935          )
   936      }
   937  `,
   938  		},
   939  		"JSON list item addition": {
   940  			Action: plans.Update,
   941  			Mode:   addrs.ManagedResourceMode,
   942  			Before: cty.ObjectVal(map[string]cty.Value{
   943  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   944  				"json_field": cty.StringVal(`["first","second"]`),
   945  			}),
   946  			After: cty.ObjectVal(map[string]cty.Value{
   947  				"id":         cty.UnknownVal(cty.String),
   948  				"json_field": cty.StringVal(`["first","second","third"]`),
   949  			}),
   950  			Schema: &configschema.Block{
   951  				Attributes: map[string]*configschema.Attribute{
   952  					"id":         {Type: cty.String, Optional: true, Computed: true},
   953  					"json_field": {Type: cty.String, Optional: true},
   954  				},
   955  			},
   956  			RequiredReplace: cty.NewPathSet(),
   957  			ExpectedOutput: `  # test_instance.example will be updated in-place
   958    ~ resource "test_instance" "example" {
   959        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   960        ~ json_field = jsonencode(
   961            ~ [
   962                  # (1 unchanged element hidden)
   963                  "second",
   964                + "third",
   965              ]
   966          )
   967      }
   968  `,
   969  		},
   970  		"JSON list object addition": {
   971  			Action: plans.Update,
   972  			Mode:   addrs.ManagedResourceMode,
   973  			Before: cty.ObjectVal(map[string]cty.Value{
   974  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   975  				"json_field": cty.StringVal(`{"first":"111"}`),
   976  			}),
   977  			After: cty.ObjectVal(map[string]cty.Value{
   978  				"id":         cty.UnknownVal(cty.String),
   979  				"json_field": cty.StringVal(`{"first":"111","second":"222"}`),
   980  			}),
   981  			Schema: &configschema.Block{
   982  				Attributes: map[string]*configschema.Attribute{
   983  					"id":         {Type: cty.String, Optional: true, Computed: true},
   984  					"json_field": {Type: cty.String, Optional: true},
   985  				},
   986  			},
   987  			RequiredReplace: cty.NewPathSet(),
   988  			ExpectedOutput: `  # test_instance.example will be updated in-place
   989    ~ resource "test_instance" "example" {
   990        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   991        ~ json_field = jsonencode(
   992            ~ {
   993                + second = "222"
   994                  # (1 unchanged element hidden)
   995              }
   996          )
   997      }
   998  `,
   999  		},
  1000  		"JSON object with nested list": {
  1001  			Action: plans.Update,
  1002  			Mode:   addrs.ManagedResourceMode,
  1003  			Before: cty.ObjectVal(map[string]cty.Value{
  1004  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  1005  				"json_field": cty.StringVal(`{
  1006  		  "Statement": ["first"]
  1007  		}`),
  1008  			}),
  1009  			After: cty.ObjectVal(map[string]cty.Value{
  1010  				"id": cty.UnknownVal(cty.String),
  1011  				"json_field": cty.StringVal(`{
  1012  		  "Statement": ["first", "second"]
  1013  		}`),
  1014  			}),
  1015  			Schema: &configschema.Block{
  1016  				Attributes: map[string]*configschema.Attribute{
  1017  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1018  					"json_field": {Type: cty.String, Optional: true},
  1019  				},
  1020  			},
  1021  			RequiredReplace: cty.NewPathSet(),
  1022  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1023    ~ resource "test_instance" "example" {
  1024        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1025        ~ json_field = jsonencode(
  1026            ~ {
  1027                ~ Statement = [
  1028                      "first",
  1029                    + "second",
  1030                  ]
  1031              }
  1032          )
  1033      }
  1034  `,
  1035  		},
  1036  		"JSON list of objects - adding item": {
  1037  			Action: plans.Update,
  1038  			Mode:   addrs.ManagedResourceMode,
  1039  			Before: cty.ObjectVal(map[string]cty.Value{
  1040  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1041  				"json_field": cty.StringVal(`[{"one": "111"}]`),
  1042  			}),
  1043  			After: cty.ObjectVal(map[string]cty.Value{
  1044  				"id":         cty.UnknownVal(cty.String),
  1045  				"json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}]`),
  1046  			}),
  1047  			Schema: &configschema.Block{
  1048  				Attributes: map[string]*configschema.Attribute{
  1049  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1050  					"json_field": {Type: cty.String, Optional: true},
  1051  				},
  1052  			},
  1053  			RequiredReplace: cty.NewPathSet(),
  1054  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1055    ~ resource "test_instance" "example" {
  1056        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1057        ~ json_field = jsonencode(
  1058            ~ [
  1059                  {
  1060                      one = "111"
  1061                  },
  1062                + {
  1063                    + two = "222"
  1064                  },
  1065              ]
  1066          )
  1067      }
  1068  `,
  1069  		},
  1070  		"JSON list of objects - removing item": {
  1071  			Action: plans.Update,
  1072  			Mode:   addrs.ManagedResourceMode,
  1073  			Before: cty.ObjectVal(map[string]cty.Value{
  1074  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1075  				"json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}, {"three": "333"}]`),
  1076  			}),
  1077  			After: cty.ObjectVal(map[string]cty.Value{
  1078  				"id":         cty.UnknownVal(cty.String),
  1079  				"json_field": cty.StringVal(`[{"one": "111"}, {"three": "333"}]`),
  1080  			}),
  1081  			Schema: &configschema.Block{
  1082  				Attributes: map[string]*configschema.Attribute{
  1083  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1084  					"json_field": {Type: cty.String, Optional: true},
  1085  				},
  1086  			},
  1087  			RequiredReplace: cty.NewPathSet(),
  1088  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1089    ~ resource "test_instance" "example" {
  1090        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1091        ~ json_field = jsonencode(
  1092            ~ [
  1093                  {
  1094                      one = "111"
  1095                  },
  1096                - {
  1097                    - two = "222"
  1098                  },
  1099                  {
  1100                      three = "333"
  1101                  },
  1102              ]
  1103          )
  1104      }
  1105  `,
  1106  		},
  1107  		"JSON object with list of objects": {
  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(`{"parent":[{"one": "111"}]}`),
  1113  			}),
  1114  			After: cty.ObjectVal(map[string]cty.Value{
  1115  				"id":         cty.UnknownVal(cty.String),
  1116  				"json_field": cty.StringVal(`{"parent":[{"one": "111"}, {"two": "222"}]}`),
  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                ~ parent = [
  1131                      {
  1132                          one = "111"
  1133                      },
  1134                    + {
  1135                        + two = "222"
  1136                      },
  1137                  ]
  1138              }
  1139          )
  1140      }
  1141  `,
  1142  		},
  1143  		"JSON object double nested lists": {
  1144  			Action: plans.Update,
  1145  			Mode:   addrs.ManagedResourceMode,
  1146  			Before: cty.ObjectVal(map[string]cty.Value{
  1147  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1148  				"json_field": cty.StringVal(`{"parent":[{"another_list": ["111"]}]}`),
  1149  			}),
  1150  			After: cty.ObjectVal(map[string]cty.Value{
  1151  				"id":         cty.UnknownVal(cty.String),
  1152  				"json_field": cty.StringVal(`{"parent":[{"another_list": ["111", "222"]}]}`),
  1153  			}),
  1154  			Schema: &configschema.Block{
  1155  				Attributes: map[string]*configschema.Attribute{
  1156  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1157  					"json_field": {Type: cty.String, Optional: true},
  1158  				},
  1159  			},
  1160  			RequiredReplace: cty.NewPathSet(),
  1161  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1162    ~ resource "test_instance" "example" {
  1163        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1164        ~ json_field = jsonencode(
  1165            ~ {
  1166                ~ parent = [
  1167                    ~ {
  1168                        ~ another_list = [
  1169                              "111",
  1170                            + "222",
  1171                          ]
  1172                      },
  1173                  ]
  1174              }
  1175          )
  1176      }
  1177  `,
  1178  		},
  1179  		"in-place update from object to tuple": {
  1180  			Action: plans.Update,
  1181  			Mode:   addrs.ManagedResourceMode,
  1182  			Before: cty.ObjectVal(map[string]cty.Value{
  1183  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1184  				"json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`),
  1185  			}),
  1186  			After: cty.ObjectVal(map[string]cty.Value{
  1187  				"id":         cty.UnknownVal(cty.String),
  1188  				"json_field": cty.StringVal(`["aaa", 42, "something"]`),
  1189  			}),
  1190  			Schema: &configschema.Block{
  1191  				Attributes: map[string]*configschema.Attribute{
  1192  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1193  					"json_field": {Type: cty.String, Optional: true},
  1194  				},
  1195  			},
  1196  			RequiredReplace: cty.NewPathSet(),
  1197  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1198    ~ resource "test_instance" "example" {
  1199        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1200        ~ json_field = jsonencode(
  1201            ~ {
  1202                - aaa = [
  1203                    - 42,
  1204                    - {
  1205                        - foo = "bar"
  1206                      },
  1207                    - "value",
  1208                  ]
  1209              } -> [
  1210                + "aaa",
  1211                + 42,
  1212                + "something",
  1213              ]
  1214          )
  1215      }
  1216  `,
  1217  		},
  1218  	}
  1219  	runTestCases(t, testCases)
  1220  }
  1221  
  1222  func TestResourceChange_listObject(t *testing.T) {
  1223  	testCases := map[string]testCase{
  1224  		// https://github.com/cycloidio/terraform/issues/30641
  1225  		"updating non-identifying attribute": {
  1226  			Action: plans.Update,
  1227  			Mode:   addrs.ManagedResourceMode,
  1228  			Before: cty.ObjectVal(map[string]cty.Value{
  1229  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  1230  				"accounts": cty.ListVal([]cty.Value{
  1231  					cty.ObjectVal(map[string]cty.Value{
  1232  						"id":     cty.StringVal("1"),
  1233  						"name":   cty.StringVal("production"),
  1234  						"status": cty.StringVal("ACTIVE"),
  1235  					}),
  1236  					cty.ObjectVal(map[string]cty.Value{
  1237  						"id":     cty.StringVal("2"),
  1238  						"name":   cty.StringVal("staging"),
  1239  						"status": cty.StringVal("ACTIVE"),
  1240  					}),
  1241  					cty.ObjectVal(map[string]cty.Value{
  1242  						"id":     cty.StringVal("3"),
  1243  						"name":   cty.StringVal("disaster-recovery"),
  1244  						"status": cty.StringVal("ACTIVE"),
  1245  					}),
  1246  				}),
  1247  			}),
  1248  			After: cty.ObjectVal(map[string]cty.Value{
  1249  				"id": cty.UnknownVal(cty.String),
  1250  				"accounts": cty.ListVal([]cty.Value{
  1251  					cty.ObjectVal(map[string]cty.Value{
  1252  						"id":     cty.StringVal("1"),
  1253  						"name":   cty.StringVal("production"),
  1254  						"status": cty.StringVal("ACTIVE"),
  1255  					}),
  1256  					cty.ObjectVal(map[string]cty.Value{
  1257  						"id":     cty.StringVal("2"),
  1258  						"name":   cty.StringVal("staging"),
  1259  						"status": cty.StringVal("EXPLODED"),
  1260  					}),
  1261  					cty.ObjectVal(map[string]cty.Value{
  1262  						"id":     cty.StringVal("3"),
  1263  						"name":   cty.StringVal("disaster-recovery"),
  1264  						"status": cty.StringVal("ACTIVE"),
  1265  					}),
  1266  				}),
  1267  			}),
  1268  			Schema: &configschema.Block{
  1269  				Attributes: map[string]*configschema.Attribute{
  1270  					"id": {Type: cty.String, Optional: true, Computed: true},
  1271  					"accounts": {
  1272  						Type: cty.List(cty.Object(map[string]cty.Type{
  1273  							"id":     cty.String,
  1274  							"name":   cty.String,
  1275  							"status": cty.String,
  1276  						})),
  1277  					},
  1278  				},
  1279  			},
  1280  			RequiredReplace: cty.NewPathSet(),
  1281  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1282    ~ resource "test_instance" "example" {
  1283        ~ accounts = [
  1284              {
  1285                  id     = "1"
  1286                  name   = "production"
  1287                  status = "ACTIVE"
  1288              },
  1289            ~ {
  1290                  id     = "2"
  1291                  name   = "staging"
  1292                ~ status = "ACTIVE" -> "EXPLODED"
  1293              },
  1294              {
  1295                  id     = "3"
  1296                  name   = "disaster-recovery"
  1297                  status = "ACTIVE"
  1298              },
  1299          ]
  1300        ~ id       = "i-02ae66f368e8518a9" -> (known after apply)
  1301      }
  1302  `,
  1303  		},
  1304  	}
  1305  	runTestCases(t, testCases)
  1306  }
  1307  
  1308  func TestResourceChange_primitiveList(t *testing.T) {
  1309  	testCases := map[string]testCase{
  1310  		"in-place update - creation": {
  1311  			Action: plans.Update,
  1312  			Mode:   addrs.ManagedResourceMode,
  1313  			Before: cty.ObjectVal(map[string]cty.Value{
  1314  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1315  				"ami":        cty.StringVal("ami-STATIC"),
  1316  				"list_field": cty.NullVal(cty.List(cty.String)),
  1317  			}),
  1318  			After: cty.ObjectVal(map[string]cty.Value{
  1319  				"id":  cty.UnknownVal(cty.String),
  1320  				"ami": cty.StringVal("ami-STATIC"),
  1321  				"list_field": cty.ListVal([]cty.Value{
  1322  					cty.StringVal("new-element"),
  1323  				}),
  1324  			}),
  1325  			Schema: &configschema.Block{
  1326  				Attributes: map[string]*configschema.Attribute{
  1327  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1328  					"ami":        {Type: cty.String, Optional: true},
  1329  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1330  				},
  1331  			},
  1332  			RequiredReplace: cty.NewPathSet(),
  1333  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1334    ~ resource "test_instance" "example" {
  1335        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1336        + list_field = [
  1337            + "new-element",
  1338          ]
  1339          # (1 unchanged attribute hidden)
  1340      }
  1341  `,
  1342  		},
  1343  		"in-place update - first addition": {
  1344  			Action: plans.Update,
  1345  			Mode:   addrs.ManagedResourceMode,
  1346  			Before: cty.ObjectVal(map[string]cty.Value{
  1347  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1348  				"ami":        cty.StringVal("ami-STATIC"),
  1349  				"list_field": cty.ListValEmpty(cty.String),
  1350  			}),
  1351  			After: cty.ObjectVal(map[string]cty.Value{
  1352  				"id":  cty.UnknownVal(cty.String),
  1353  				"ami": cty.StringVal("ami-STATIC"),
  1354  				"list_field": cty.ListVal([]cty.Value{
  1355  					cty.StringVal("new-element"),
  1356  				}),
  1357  			}),
  1358  			Schema: &configschema.Block{
  1359  				Attributes: map[string]*configschema.Attribute{
  1360  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1361  					"ami":        {Type: cty.String, Optional: true},
  1362  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1363  				},
  1364  			},
  1365  			RequiredReplace: cty.NewPathSet(),
  1366  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1367    ~ resource "test_instance" "example" {
  1368        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1369        ~ list_field = [
  1370            + "new-element",
  1371          ]
  1372          # (1 unchanged attribute hidden)
  1373      }
  1374  `,
  1375  		},
  1376  		"in-place update - insertion": {
  1377  			Action: plans.Update,
  1378  			Mode:   addrs.ManagedResourceMode,
  1379  			Before: cty.ObjectVal(map[string]cty.Value{
  1380  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1381  				"ami": cty.StringVal("ami-STATIC"),
  1382  				"list_field": cty.ListVal([]cty.Value{
  1383  					cty.StringVal("aaaa"),
  1384  					cty.StringVal("bbbb"),
  1385  					cty.StringVal("dddd"),
  1386  					cty.StringVal("eeee"),
  1387  					cty.StringVal("ffff"),
  1388  				}),
  1389  			}),
  1390  			After: cty.ObjectVal(map[string]cty.Value{
  1391  				"id":  cty.UnknownVal(cty.String),
  1392  				"ami": cty.StringVal("ami-STATIC"),
  1393  				"list_field": cty.ListVal([]cty.Value{
  1394  					cty.StringVal("aaaa"),
  1395  					cty.StringVal("bbbb"),
  1396  					cty.StringVal("cccc"),
  1397  					cty.StringVal("dddd"),
  1398  					cty.StringVal("eeee"),
  1399  					cty.StringVal("ffff"),
  1400  				}),
  1401  			}),
  1402  			Schema: &configschema.Block{
  1403  				Attributes: map[string]*configschema.Attribute{
  1404  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1405  					"ami":        {Type: cty.String, Optional: true},
  1406  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1407  				},
  1408  			},
  1409  			RequiredReplace: cty.NewPathSet(),
  1410  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1411    ~ resource "test_instance" "example" {
  1412        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1413        ~ list_field = [
  1414              # (1 unchanged element hidden)
  1415              "bbbb",
  1416            + "cccc",
  1417              "dddd",
  1418              # (2 unchanged elements hidden)
  1419          ]
  1420          # (1 unchanged attribute hidden)
  1421      }
  1422  `,
  1423  		},
  1424  		"force-new update - insertion": {
  1425  			Action:       plans.DeleteThenCreate,
  1426  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  1427  			Mode:         addrs.ManagedResourceMode,
  1428  			Before: cty.ObjectVal(map[string]cty.Value{
  1429  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1430  				"ami": cty.StringVal("ami-STATIC"),
  1431  				"list_field": cty.ListVal([]cty.Value{
  1432  					cty.StringVal("aaaa"),
  1433  					cty.StringVal("cccc"),
  1434  				}),
  1435  			}),
  1436  			After: cty.ObjectVal(map[string]cty.Value{
  1437  				"id":  cty.UnknownVal(cty.String),
  1438  				"ami": cty.StringVal("ami-STATIC"),
  1439  				"list_field": cty.ListVal([]cty.Value{
  1440  					cty.StringVal("aaaa"),
  1441  					cty.StringVal("bbbb"),
  1442  					cty.StringVal("cccc"),
  1443  				}),
  1444  			}),
  1445  			Schema: &configschema.Block{
  1446  				Attributes: map[string]*configschema.Attribute{
  1447  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1448  					"ami":        {Type: cty.String, Optional: true},
  1449  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1450  				},
  1451  			},
  1452  			RequiredReplace: cty.NewPathSet(cty.Path{
  1453  				cty.GetAttrStep{Name: "list_field"},
  1454  			}),
  1455  			ExpectedOutput: `  # test_instance.example must be replaced
  1456  -/+ resource "test_instance" "example" {
  1457        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1458        ~ list_field = [ # forces replacement
  1459              "aaaa",
  1460            + "bbbb",
  1461              "cccc",
  1462          ]
  1463          # (1 unchanged attribute hidden)
  1464      }
  1465  `,
  1466  		},
  1467  		"in-place update - deletion": {
  1468  			Action: plans.Update,
  1469  			Mode:   addrs.ManagedResourceMode,
  1470  			Before: cty.ObjectVal(map[string]cty.Value{
  1471  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1472  				"ami": cty.StringVal("ami-STATIC"),
  1473  				"list_field": cty.ListVal([]cty.Value{
  1474  					cty.StringVal("aaaa"),
  1475  					cty.StringVal("bbbb"),
  1476  					cty.StringVal("cccc"),
  1477  					cty.StringVal("dddd"),
  1478  					cty.StringVal("eeee"),
  1479  				}),
  1480  			}),
  1481  			After: cty.ObjectVal(map[string]cty.Value{
  1482  				"id":  cty.UnknownVal(cty.String),
  1483  				"ami": cty.StringVal("ami-STATIC"),
  1484  				"list_field": cty.ListVal([]cty.Value{
  1485  					cty.StringVal("bbbb"),
  1486  					cty.StringVal("dddd"),
  1487  					cty.StringVal("eeee"),
  1488  				}),
  1489  			}),
  1490  			Schema: &configschema.Block{
  1491  				Attributes: map[string]*configschema.Attribute{
  1492  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1493  					"ami":        {Type: cty.String, Optional: true},
  1494  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1495  				},
  1496  			},
  1497  			RequiredReplace: cty.NewPathSet(),
  1498  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1499    ~ resource "test_instance" "example" {
  1500        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1501        ~ list_field = [
  1502            - "aaaa",
  1503              "bbbb",
  1504            - "cccc",
  1505              "dddd",
  1506              # (1 unchanged element hidden)
  1507          ]
  1508          # (1 unchanged attribute hidden)
  1509      }
  1510  `,
  1511  		},
  1512  		"creation - empty list": {
  1513  			Action: plans.Create,
  1514  			Mode:   addrs.ManagedResourceMode,
  1515  			Before: cty.NullVal(cty.EmptyObject),
  1516  			After: cty.ObjectVal(map[string]cty.Value{
  1517  				"id":         cty.UnknownVal(cty.String),
  1518  				"ami":        cty.StringVal("ami-STATIC"),
  1519  				"list_field": cty.ListValEmpty(cty.String),
  1520  			}),
  1521  			Schema: &configschema.Block{
  1522  				Attributes: map[string]*configschema.Attribute{
  1523  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1524  					"ami":        {Type: cty.String, Optional: true},
  1525  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1526  				},
  1527  			},
  1528  			RequiredReplace: cty.NewPathSet(),
  1529  			ExpectedOutput: `  # test_instance.example will be created
  1530    + resource "test_instance" "example" {
  1531        + ami        = "ami-STATIC"
  1532        + id         = (known after apply)
  1533        + list_field = []
  1534      }
  1535  `,
  1536  		},
  1537  		"in-place update - full to empty": {
  1538  			Action: plans.Update,
  1539  			Mode:   addrs.ManagedResourceMode,
  1540  			Before: cty.ObjectVal(map[string]cty.Value{
  1541  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1542  				"ami": cty.StringVal("ami-STATIC"),
  1543  				"list_field": cty.ListVal([]cty.Value{
  1544  					cty.StringVal("aaaa"),
  1545  					cty.StringVal("bbbb"),
  1546  					cty.StringVal("cccc"),
  1547  				}),
  1548  			}),
  1549  			After: cty.ObjectVal(map[string]cty.Value{
  1550  				"id":         cty.UnknownVal(cty.String),
  1551  				"ami":        cty.StringVal("ami-STATIC"),
  1552  				"list_field": cty.ListValEmpty(cty.String),
  1553  			}),
  1554  			Schema: &configschema.Block{
  1555  				Attributes: map[string]*configschema.Attribute{
  1556  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1557  					"ami":        {Type: cty.String, Optional: true},
  1558  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1559  				},
  1560  			},
  1561  			RequiredReplace: cty.NewPathSet(),
  1562  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1563    ~ resource "test_instance" "example" {
  1564        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1565        ~ list_field = [
  1566            - "aaaa",
  1567            - "bbbb",
  1568            - "cccc",
  1569          ]
  1570          # (1 unchanged attribute hidden)
  1571      }
  1572  `,
  1573  		},
  1574  		"in-place update - null to empty": {
  1575  			Action: plans.Update,
  1576  			Mode:   addrs.ManagedResourceMode,
  1577  			Before: cty.ObjectVal(map[string]cty.Value{
  1578  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1579  				"ami":        cty.StringVal("ami-STATIC"),
  1580  				"list_field": cty.NullVal(cty.List(cty.String)),
  1581  			}),
  1582  			After: cty.ObjectVal(map[string]cty.Value{
  1583  				"id":         cty.UnknownVal(cty.String),
  1584  				"ami":        cty.StringVal("ami-STATIC"),
  1585  				"list_field": cty.ListValEmpty(cty.String),
  1586  			}),
  1587  			Schema: &configschema.Block{
  1588  				Attributes: map[string]*configschema.Attribute{
  1589  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1590  					"ami":        {Type: cty.String, Optional: true},
  1591  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1592  				},
  1593  			},
  1594  			RequiredReplace: cty.NewPathSet(),
  1595  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1596    ~ resource "test_instance" "example" {
  1597        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1598        + list_field = []
  1599          # (1 unchanged attribute hidden)
  1600      }
  1601  `,
  1602  		},
  1603  		"update to unknown element": {
  1604  			Action: plans.Update,
  1605  			Mode:   addrs.ManagedResourceMode,
  1606  			Before: cty.ObjectVal(map[string]cty.Value{
  1607  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1608  				"ami": cty.StringVal("ami-STATIC"),
  1609  				"list_field": cty.ListVal([]cty.Value{
  1610  					cty.StringVal("aaaa"),
  1611  					cty.StringVal("bbbb"),
  1612  					cty.StringVal("cccc"),
  1613  				}),
  1614  			}),
  1615  			After: cty.ObjectVal(map[string]cty.Value{
  1616  				"id":  cty.UnknownVal(cty.String),
  1617  				"ami": cty.StringVal("ami-STATIC"),
  1618  				"list_field": cty.ListVal([]cty.Value{
  1619  					cty.StringVal("aaaa"),
  1620  					cty.UnknownVal(cty.String),
  1621  					cty.StringVal("cccc"),
  1622  				}),
  1623  			}),
  1624  			Schema: &configschema.Block{
  1625  				Attributes: map[string]*configschema.Attribute{
  1626  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1627  					"ami":        {Type: cty.String, Optional: true},
  1628  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1629  				},
  1630  			},
  1631  			RequiredReplace: cty.NewPathSet(),
  1632  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1633    ~ resource "test_instance" "example" {
  1634        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1635        ~ list_field = [
  1636              "aaaa",
  1637            - "bbbb",
  1638            + (known after apply),
  1639              "cccc",
  1640          ]
  1641          # (1 unchanged attribute hidden)
  1642      }
  1643  `,
  1644  		},
  1645  		"update - two new unknown elements": {
  1646  			Action: plans.Update,
  1647  			Mode:   addrs.ManagedResourceMode,
  1648  			Before: cty.ObjectVal(map[string]cty.Value{
  1649  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1650  				"ami": cty.StringVal("ami-STATIC"),
  1651  				"list_field": cty.ListVal([]cty.Value{
  1652  					cty.StringVal("aaaa"),
  1653  					cty.StringVal("bbbb"),
  1654  					cty.StringVal("cccc"),
  1655  					cty.StringVal("dddd"),
  1656  					cty.StringVal("eeee"),
  1657  				}),
  1658  			}),
  1659  			After: cty.ObjectVal(map[string]cty.Value{
  1660  				"id":  cty.UnknownVal(cty.String),
  1661  				"ami": cty.StringVal("ami-STATIC"),
  1662  				"list_field": cty.ListVal([]cty.Value{
  1663  					cty.StringVal("aaaa"),
  1664  					cty.UnknownVal(cty.String),
  1665  					cty.UnknownVal(cty.String),
  1666  					cty.StringVal("cccc"),
  1667  					cty.StringVal("dddd"),
  1668  					cty.StringVal("eeee"),
  1669  				}),
  1670  			}),
  1671  			Schema: &configschema.Block{
  1672  				Attributes: map[string]*configschema.Attribute{
  1673  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1674  					"ami":        {Type: cty.String, Optional: true},
  1675  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1676  				},
  1677  			},
  1678  			RequiredReplace: cty.NewPathSet(),
  1679  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1680    ~ resource "test_instance" "example" {
  1681        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1682        ~ list_field = [
  1683              "aaaa",
  1684            - "bbbb",
  1685            + (known after apply),
  1686            + (known after apply),
  1687              "cccc",
  1688              # (2 unchanged elements hidden)
  1689          ]
  1690          # (1 unchanged attribute hidden)
  1691      }
  1692  `,
  1693  		},
  1694  	}
  1695  	runTestCases(t, testCases)
  1696  }
  1697  
  1698  func TestResourceChange_primitiveTuple(t *testing.T) {
  1699  	testCases := map[string]testCase{
  1700  		"in-place update": {
  1701  			Action: plans.Update,
  1702  			Mode:   addrs.ManagedResourceMode,
  1703  			Before: cty.ObjectVal(map[string]cty.Value{
  1704  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  1705  				"tuple_field": cty.TupleVal([]cty.Value{
  1706  					cty.StringVal("aaaa"),
  1707  					cty.StringVal("bbbb"),
  1708  					cty.StringVal("dddd"),
  1709  					cty.StringVal("eeee"),
  1710  					cty.StringVal("ffff"),
  1711  				}),
  1712  			}),
  1713  			After: cty.ObjectVal(map[string]cty.Value{
  1714  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  1715  				"tuple_field": cty.TupleVal([]cty.Value{
  1716  					cty.StringVal("aaaa"),
  1717  					cty.StringVal("bbbb"),
  1718  					cty.StringVal("cccc"),
  1719  					cty.StringVal("eeee"),
  1720  					cty.StringVal("ffff"),
  1721  				}),
  1722  			}),
  1723  			Schema: &configschema.Block{
  1724  				Attributes: map[string]*configschema.Attribute{
  1725  					"id":          {Type: cty.String, Required: true},
  1726  					"tuple_field": {Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.String, cty.String, cty.String}), Optional: true},
  1727  				},
  1728  			},
  1729  			RequiredReplace: cty.NewPathSet(),
  1730  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1731    ~ resource "test_instance" "example" {
  1732          id          = "i-02ae66f368e8518a9"
  1733        ~ tuple_field = [
  1734              # (1 unchanged element hidden)
  1735              "bbbb",
  1736            - "dddd",
  1737            + "cccc",
  1738              "eeee",
  1739              # (1 unchanged element hidden)
  1740          ]
  1741      }
  1742  `,
  1743  		},
  1744  	}
  1745  	runTestCases(t, testCases)
  1746  }
  1747  
  1748  func TestResourceChange_primitiveSet(t *testing.T) {
  1749  	testCases := map[string]testCase{
  1750  		"in-place update - creation": {
  1751  			Action: plans.Update,
  1752  			Mode:   addrs.ManagedResourceMode,
  1753  			Before: cty.ObjectVal(map[string]cty.Value{
  1754  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
  1755  				"ami":       cty.StringVal("ami-STATIC"),
  1756  				"set_field": cty.NullVal(cty.Set(cty.String)),
  1757  			}),
  1758  			After: cty.ObjectVal(map[string]cty.Value{
  1759  				"id":  cty.UnknownVal(cty.String),
  1760  				"ami": cty.StringVal("ami-STATIC"),
  1761  				"set_field": cty.SetVal([]cty.Value{
  1762  					cty.StringVal("new-element"),
  1763  				}),
  1764  			}),
  1765  			Schema: &configschema.Block{
  1766  				Attributes: map[string]*configschema.Attribute{
  1767  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1768  					"ami":       {Type: cty.String, Optional: true},
  1769  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  1770  				},
  1771  			},
  1772  			RequiredReplace: cty.NewPathSet(),
  1773  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1774    ~ resource "test_instance" "example" {
  1775        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1776        + set_field = [
  1777            + "new-element",
  1778          ]
  1779          # (1 unchanged attribute hidden)
  1780      }
  1781  `,
  1782  		},
  1783  		"in-place update - first insertion": {
  1784  			Action: plans.Update,
  1785  			Mode:   addrs.ManagedResourceMode,
  1786  			Before: cty.ObjectVal(map[string]cty.Value{
  1787  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
  1788  				"ami":       cty.StringVal("ami-STATIC"),
  1789  				"set_field": cty.SetValEmpty(cty.String),
  1790  			}),
  1791  			After: cty.ObjectVal(map[string]cty.Value{
  1792  				"id":  cty.UnknownVal(cty.String),
  1793  				"ami": cty.StringVal("ami-STATIC"),
  1794  				"set_field": cty.SetVal([]cty.Value{
  1795  					cty.StringVal("new-element"),
  1796  				}),
  1797  			}),
  1798  			Schema: &configschema.Block{
  1799  				Attributes: map[string]*configschema.Attribute{
  1800  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1801  					"ami":       {Type: cty.String, Optional: true},
  1802  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  1803  				},
  1804  			},
  1805  			RequiredReplace: cty.NewPathSet(),
  1806  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1807    ~ resource "test_instance" "example" {
  1808        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1809        ~ set_field = [
  1810            + "new-element",
  1811          ]
  1812          # (1 unchanged attribute hidden)
  1813      }
  1814  `,
  1815  		},
  1816  		"in-place update - insertion": {
  1817  			Action: plans.Update,
  1818  			Mode:   addrs.ManagedResourceMode,
  1819  			Before: cty.ObjectVal(map[string]cty.Value{
  1820  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1821  				"ami": cty.StringVal("ami-STATIC"),
  1822  				"set_field": cty.SetVal([]cty.Value{
  1823  					cty.StringVal("aaaa"),
  1824  					cty.StringVal("cccc"),
  1825  				}),
  1826  			}),
  1827  			After: cty.ObjectVal(map[string]cty.Value{
  1828  				"id":  cty.UnknownVal(cty.String),
  1829  				"ami": cty.StringVal("ami-STATIC"),
  1830  				"set_field": cty.SetVal([]cty.Value{
  1831  					cty.StringVal("aaaa"),
  1832  					cty.StringVal("bbbb"),
  1833  					cty.StringVal("cccc"),
  1834  				}),
  1835  			}),
  1836  			Schema: &configschema.Block{
  1837  				Attributes: map[string]*configschema.Attribute{
  1838  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1839  					"ami":       {Type: cty.String, Optional: true},
  1840  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  1841  				},
  1842  			},
  1843  			RequiredReplace: cty.NewPathSet(),
  1844  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1845    ~ resource "test_instance" "example" {
  1846        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1847        ~ set_field = [
  1848            + "bbbb",
  1849              # (2 unchanged elements hidden)
  1850          ]
  1851          # (1 unchanged attribute hidden)
  1852      }
  1853  `,
  1854  		},
  1855  		"force-new update - insertion": {
  1856  			Action:       plans.DeleteThenCreate,
  1857  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  1858  			Mode:         addrs.ManagedResourceMode,
  1859  			Before: cty.ObjectVal(map[string]cty.Value{
  1860  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1861  				"ami": cty.StringVal("ami-STATIC"),
  1862  				"set_field": cty.SetVal([]cty.Value{
  1863  					cty.StringVal("aaaa"),
  1864  					cty.StringVal("cccc"),
  1865  				}),
  1866  			}),
  1867  			After: cty.ObjectVal(map[string]cty.Value{
  1868  				"id":  cty.UnknownVal(cty.String),
  1869  				"ami": cty.StringVal("ami-STATIC"),
  1870  				"set_field": cty.SetVal([]cty.Value{
  1871  					cty.StringVal("aaaa"),
  1872  					cty.StringVal("bbbb"),
  1873  					cty.StringVal("cccc"),
  1874  				}),
  1875  			}),
  1876  			Schema: &configschema.Block{
  1877  				Attributes: map[string]*configschema.Attribute{
  1878  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1879  					"ami":       {Type: cty.String, Optional: true},
  1880  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  1881  				},
  1882  			},
  1883  			RequiredReplace: cty.NewPathSet(cty.Path{
  1884  				cty.GetAttrStep{Name: "set_field"},
  1885  			}),
  1886  			ExpectedOutput: `  # test_instance.example must be replaced
  1887  -/+ resource "test_instance" "example" {
  1888        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1889        ~ set_field = [ # forces replacement
  1890            + "bbbb",
  1891              # (2 unchanged elements hidden)
  1892          ]
  1893          # (1 unchanged attribute hidden)
  1894      }
  1895  `,
  1896  		},
  1897  		"in-place update - deletion": {
  1898  			Action: plans.Update,
  1899  			Mode:   addrs.ManagedResourceMode,
  1900  			Before: cty.ObjectVal(map[string]cty.Value{
  1901  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1902  				"ami": cty.StringVal("ami-STATIC"),
  1903  				"set_field": cty.SetVal([]cty.Value{
  1904  					cty.StringVal("aaaa"),
  1905  					cty.StringVal("bbbb"),
  1906  					cty.StringVal("cccc"),
  1907  				}),
  1908  			}),
  1909  			After: cty.ObjectVal(map[string]cty.Value{
  1910  				"id":  cty.UnknownVal(cty.String),
  1911  				"ami": cty.StringVal("ami-STATIC"),
  1912  				"set_field": cty.SetVal([]cty.Value{
  1913  					cty.StringVal("bbbb"),
  1914  				}),
  1915  			}),
  1916  			Schema: &configschema.Block{
  1917  				Attributes: map[string]*configschema.Attribute{
  1918  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1919  					"ami":       {Type: cty.String, Optional: true},
  1920  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  1921  				},
  1922  			},
  1923  			RequiredReplace: cty.NewPathSet(),
  1924  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1925    ~ resource "test_instance" "example" {
  1926        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1927        ~ set_field = [
  1928            - "aaaa",
  1929            - "cccc",
  1930              # (1 unchanged element hidden)
  1931          ]
  1932          # (1 unchanged attribute hidden)
  1933      }
  1934  `,
  1935  		},
  1936  		"creation - empty set": {
  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  				"set_field": cty.SetValEmpty(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  					"set_field": {Type: cty.Set(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        + set_field = []
  1958      }
  1959  `,
  1960  		},
  1961  		"in-place update - full to empty set": {
  1962  			Action: plans.Update,
  1963  			Mode:   addrs.ManagedResourceMode,
  1964  			Before: cty.ObjectVal(map[string]cty.Value{
  1965  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1966  				"ami": cty.StringVal("ami-STATIC"),
  1967  				"set_field": cty.SetVal([]cty.Value{
  1968  					cty.StringVal("aaaa"),
  1969  					cty.StringVal("bbbb"),
  1970  				}),
  1971  			}),
  1972  			After: cty.ObjectVal(map[string]cty.Value{
  1973  				"id":        cty.UnknownVal(cty.String),
  1974  				"ami":       cty.StringVal("ami-STATIC"),
  1975  				"set_field": cty.SetValEmpty(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  					"set_field": {Type: cty.Set(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        ~ set_field = [
  1989            - "aaaa",
  1990            - "bbbb",
  1991          ]
  1992          # (1 unchanged attribute hidden)
  1993      }
  1994  `,
  1995  		},
  1996  		"in-place update - null to empty set": {
  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  				"set_field": cty.NullVal(cty.Set(cty.String)),
  2003  			}),
  2004  			After: cty.ObjectVal(map[string]cty.Value{
  2005  				"id":        cty.UnknownVal(cty.String),
  2006  				"ami":       cty.StringVal("ami-STATIC"),
  2007  				"set_field": cty.SetValEmpty(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  					"set_field": {Type: cty.Set(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        + set_field = []
  2021          # (1 unchanged attribute hidden)
  2022      }
  2023  `,
  2024  		},
  2025  		"in-place update to unknown": {
  2026  			Action: plans.Update,
  2027  			Mode:   addrs.ManagedResourceMode,
  2028  			Before: cty.ObjectVal(map[string]cty.Value{
  2029  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2030  				"ami": cty.StringVal("ami-STATIC"),
  2031  				"set_field": cty.SetVal([]cty.Value{
  2032  					cty.StringVal("aaaa"),
  2033  					cty.StringVal("bbbb"),
  2034  				}),
  2035  			}),
  2036  			After: cty.ObjectVal(map[string]cty.Value{
  2037  				"id":        cty.UnknownVal(cty.String),
  2038  				"ami":       cty.StringVal("ami-STATIC"),
  2039  				"set_field": cty.UnknownVal(cty.Set(cty.String)),
  2040  			}),
  2041  			Schema: &configschema.Block{
  2042  				Attributes: map[string]*configschema.Attribute{
  2043  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2044  					"ami":       {Type: cty.String, Optional: true},
  2045  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  2046  				},
  2047  			},
  2048  			RequiredReplace: cty.NewPathSet(),
  2049  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2050    ~ resource "test_instance" "example" {
  2051        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2052        ~ set_field = [
  2053            - "aaaa",
  2054            - "bbbb",
  2055          ] -> (known after apply)
  2056          # (1 unchanged attribute hidden)
  2057      }
  2058  `,
  2059  		},
  2060  		"in-place update to unknown element": {
  2061  			Action: plans.Update,
  2062  			Mode:   addrs.ManagedResourceMode,
  2063  			Before: cty.ObjectVal(map[string]cty.Value{
  2064  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2065  				"ami": cty.StringVal("ami-STATIC"),
  2066  				"set_field": cty.SetVal([]cty.Value{
  2067  					cty.StringVal("aaaa"),
  2068  					cty.StringVal("bbbb"),
  2069  				}),
  2070  			}),
  2071  			After: cty.ObjectVal(map[string]cty.Value{
  2072  				"id":  cty.UnknownVal(cty.String),
  2073  				"ami": cty.StringVal("ami-STATIC"),
  2074  				"set_field": cty.SetVal([]cty.Value{
  2075  					cty.StringVal("aaaa"),
  2076  					cty.UnknownVal(cty.String),
  2077  				}),
  2078  			}),
  2079  			Schema: &configschema.Block{
  2080  				Attributes: map[string]*configschema.Attribute{
  2081  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2082  					"ami":       {Type: cty.String, Optional: true},
  2083  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  2084  				},
  2085  			},
  2086  			RequiredReplace: cty.NewPathSet(),
  2087  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2088    ~ resource "test_instance" "example" {
  2089        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2090        ~ set_field = [
  2091            - "bbbb",
  2092            ~ (known after apply),
  2093              # (1 unchanged element hidden)
  2094          ]
  2095          # (1 unchanged attribute hidden)
  2096      }
  2097  `,
  2098  		},
  2099  	}
  2100  	runTestCases(t, testCases)
  2101  }
  2102  
  2103  func TestResourceChange_map(t *testing.T) {
  2104  	testCases := map[string]testCase{
  2105  		"in-place update - creation": {
  2106  			Action: plans.Update,
  2107  			Mode:   addrs.ManagedResourceMode,
  2108  			Before: cty.ObjectVal(map[string]cty.Value{
  2109  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
  2110  				"ami":       cty.StringVal("ami-STATIC"),
  2111  				"map_field": cty.NullVal(cty.Map(cty.String)),
  2112  			}),
  2113  			After: cty.ObjectVal(map[string]cty.Value{
  2114  				"id":  cty.UnknownVal(cty.String),
  2115  				"ami": cty.StringVal("ami-STATIC"),
  2116  				"map_field": cty.MapVal(map[string]cty.Value{
  2117  					"new-key": cty.StringVal("new-element"),
  2118  					"be:ep":   cty.StringVal("boop"),
  2119  				}),
  2120  			}),
  2121  			Schema: &configschema.Block{
  2122  				Attributes: map[string]*configschema.Attribute{
  2123  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2124  					"ami":       {Type: cty.String, Optional: true},
  2125  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  2126  				},
  2127  			},
  2128  			RequiredReplace: cty.NewPathSet(),
  2129  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2130    ~ resource "test_instance" "example" {
  2131        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2132        + map_field = {
  2133            + "be:ep"   = "boop"
  2134            + "new-key" = "new-element"
  2135          }
  2136          # (1 unchanged attribute hidden)
  2137      }
  2138  `,
  2139  		},
  2140  		"in-place update - first insertion": {
  2141  			Action: plans.Update,
  2142  			Mode:   addrs.ManagedResourceMode,
  2143  			Before: cty.ObjectVal(map[string]cty.Value{
  2144  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
  2145  				"ami":       cty.StringVal("ami-STATIC"),
  2146  				"map_field": cty.MapValEmpty(cty.String),
  2147  			}),
  2148  			After: cty.ObjectVal(map[string]cty.Value{
  2149  				"id":  cty.UnknownVal(cty.String),
  2150  				"ami": cty.StringVal("ami-STATIC"),
  2151  				"map_field": cty.MapVal(map[string]cty.Value{
  2152  					"new-key": cty.StringVal("new-element"),
  2153  					"be:ep":   cty.StringVal("boop"),
  2154  				}),
  2155  			}),
  2156  			Schema: &configschema.Block{
  2157  				Attributes: map[string]*configschema.Attribute{
  2158  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2159  					"ami":       {Type: cty.String, Optional: true},
  2160  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  2161  				},
  2162  			},
  2163  			RequiredReplace: cty.NewPathSet(),
  2164  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2165    ~ resource "test_instance" "example" {
  2166        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2167        ~ map_field = {
  2168            + "be:ep"   = "boop"
  2169            + "new-key" = "new-element"
  2170          }
  2171          # (1 unchanged attribute hidden)
  2172      }
  2173  `,
  2174  		},
  2175  		"in-place update - insertion": {
  2176  			Action: plans.Update,
  2177  			Mode:   addrs.ManagedResourceMode,
  2178  			Before: cty.ObjectVal(map[string]cty.Value{
  2179  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2180  				"ami": cty.StringVal("ami-STATIC"),
  2181  				"map_field": cty.MapVal(map[string]cty.Value{
  2182  					"a": cty.StringVal("aaaa"),
  2183  					"c": cty.StringVal("cccc"),
  2184  				}),
  2185  			}),
  2186  			After: cty.ObjectVal(map[string]cty.Value{
  2187  				"id":  cty.UnknownVal(cty.String),
  2188  				"ami": cty.StringVal("ami-STATIC"),
  2189  				"map_field": cty.MapVal(map[string]cty.Value{
  2190  					"a":   cty.StringVal("aaaa"),
  2191  					"b":   cty.StringVal("bbbb"),
  2192  					"b:b": cty.StringVal("bbbb"),
  2193  					"c":   cty.StringVal("cccc"),
  2194  				}),
  2195  			}),
  2196  			Schema: &configschema.Block{
  2197  				Attributes: map[string]*configschema.Attribute{
  2198  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2199  					"ami":       {Type: cty.String, Optional: true},
  2200  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  2201  				},
  2202  			},
  2203  			RequiredReplace: cty.NewPathSet(),
  2204  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2205    ~ resource "test_instance" "example" {
  2206        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2207        ~ map_field = {
  2208            + "b"   = "bbbb"
  2209            + "b:b" = "bbbb"
  2210              # (2 unchanged elements hidden)
  2211          }
  2212          # (1 unchanged attribute hidden)
  2213      }
  2214  `,
  2215  		},
  2216  		"force-new update - insertion": {
  2217  			Action:       plans.DeleteThenCreate,
  2218  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  2219  			Mode:         addrs.ManagedResourceMode,
  2220  			Before: cty.ObjectVal(map[string]cty.Value{
  2221  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2222  				"ami": cty.StringVal("ami-STATIC"),
  2223  				"map_field": cty.MapVal(map[string]cty.Value{
  2224  					"a": cty.StringVal("aaaa"),
  2225  					"c": cty.StringVal("cccc"),
  2226  				}),
  2227  			}),
  2228  			After: cty.ObjectVal(map[string]cty.Value{
  2229  				"id":  cty.UnknownVal(cty.String),
  2230  				"ami": cty.StringVal("ami-STATIC"),
  2231  				"map_field": cty.MapVal(map[string]cty.Value{
  2232  					"a": cty.StringVal("aaaa"),
  2233  					"b": cty.StringVal("bbbb"),
  2234  					"c": cty.StringVal("cccc"),
  2235  				}),
  2236  			}),
  2237  			Schema: &configschema.Block{
  2238  				Attributes: map[string]*configschema.Attribute{
  2239  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2240  					"ami":       {Type: cty.String, Optional: true},
  2241  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  2242  				},
  2243  			},
  2244  			RequiredReplace: cty.NewPathSet(cty.Path{
  2245  				cty.GetAttrStep{Name: "map_field"},
  2246  			}),
  2247  			ExpectedOutput: `  # test_instance.example must be replaced
  2248  -/+ resource "test_instance" "example" {
  2249        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2250        ~ map_field = { # forces replacement
  2251            + "b" = "bbbb"
  2252              # (2 unchanged elements hidden)
  2253          }
  2254          # (1 unchanged attribute hidden)
  2255      }
  2256  `,
  2257  		},
  2258  		"in-place update - deletion": {
  2259  			Action: plans.Update,
  2260  			Mode:   addrs.ManagedResourceMode,
  2261  			Before: cty.ObjectVal(map[string]cty.Value{
  2262  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2263  				"ami": cty.StringVal("ami-STATIC"),
  2264  				"map_field": cty.MapVal(map[string]cty.Value{
  2265  					"a": cty.StringVal("aaaa"),
  2266  					"b": cty.StringVal("bbbb"),
  2267  					"c": cty.StringVal("cccc"),
  2268  				}),
  2269  			}),
  2270  			After: cty.ObjectVal(map[string]cty.Value{
  2271  				"id":  cty.UnknownVal(cty.String),
  2272  				"ami": cty.StringVal("ami-STATIC"),
  2273  				"map_field": cty.MapVal(map[string]cty.Value{
  2274  					"b": cty.StringVal("bbbb"),
  2275  				}),
  2276  			}),
  2277  			Schema: &configschema.Block{
  2278  				Attributes: map[string]*configschema.Attribute{
  2279  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2280  					"ami":       {Type: cty.String, Optional: true},
  2281  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  2282  				},
  2283  			},
  2284  			RequiredReplace: cty.NewPathSet(),
  2285  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2286    ~ resource "test_instance" "example" {
  2287        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2288        ~ map_field = {
  2289            - "a" = "aaaa" -> null
  2290            - "c" = "cccc" -> null
  2291              # (1 unchanged element hidden)
  2292          }
  2293          # (1 unchanged attribute hidden)
  2294      }
  2295  `,
  2296  		},
  2297  		"creation - empty": {
  2298  			Action: plans.Create,
  2299  			Mode:   addrs.ManagedResourceMode,
  2300  			Before: cty.NullVal(cty.EmptyObject),
  2301  			After: cty.ObjectVal(map[string]cty.Value{
  2302  				"id":        cty.UnknownVal(cty.String),
  2303  				"ami":       cty.StringVal("ami-STATIC"),
  2304  				"map_field": cty.MapValEmpty(cty.String),
  2305  			}),
  2306  			Schema: &configschema.Block{
  2307  				Attributes: map[string]*configschema.Attribute{
  2308  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2309  					"ami":       {Type: cty.String, Optional: true},
  2310  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  2311  				},
  2312  			},
  2313  			RequiredReplace: cty.NewPathSet(),
  2314  			ExpectedOutput: `  # test_instance.example will be created
  2315    + resource "test_instance" "example" {
  2316        + ami       = "ami-STATIC"
  2317        + id        = (known after apply)
  2318        + map_field = {}
  2319      }
  2320  `,
  2321  		},
  2322  		"update to unknown element": {
  2323  			Action: plans.Update,
  2324  			Mode:   addrs.ManagedResourceMode,
  2325  			Before: cty.ObjectVal(map[string]cty.Value{
  2326  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2327  				"ami": cty.StringVal("ami-STATIC"),
  2328  				"map_field": cty.MapVal(map[string]cty.Value{
  2329  					"a": cty.StringVal("aaaa"),
  2330  					"b": cty.StringVal("bbbb"),
  2331  					"c": cty.StringVal("cccc"),
  2332  				}),
  2333  			}),
  2334  			After: cty.ObjectVal(map[string]cty.Value{
  2335  				"id":  cty.UnknownVal(cty.String),
  2336  				"ami": cty.StringVal("ami-STATIC"),
  2337  				"map_field": cty.MapVal(map[string]cty.Value{
  2338  					"a": cty.StringVal("aaaa"),
  2339  					"b": cty.UnknownVal(cty.String),
  2340  					"c": cty.StringVal("cccc"),
  2341  				}),
  2342  			}),
  2343  			Schema: &configschema.Block{
  2344  				Attributes: map[string]*configschema.Attribute{
  2345  					"id":        {Type: cty.String, Optional: true, Computed: true},
  2346  					"ami":       {Type: cty.String, Optional: true},
  2347  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  2348  				},
  2349  			},
  2350  			RequiredReplace: cty.NewPathSet(),
  2351  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2352    ~ resource "test_instance" "example" {
  2353        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2354        ~ map_field = {
  2355            ~ "b" = "bbbb" -> (known after apply)
  2356              # (2 unchanged elements hidden)
  2357          }
  2358          # (1 unchanged attribute hidden)
  2359      }
  2360  `,
  2361  		},
  2362  	}
  2363  	runTestCases(t, testCases)
  2364  }
  2365  
  2366  func TestResourceChange_nestedList(t *testing.T) {
  2367  	testCases := map[string]testCase{
  2368  		"in-place update - equal": {
  2369  			Action: plans.Update,
  2370  			Mode:   addrs.ManagedResourceMode,
  2371  			Before: cty.ObjectVal(map[string]cty.Value{
  2372  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2373  				"ami": cty.StringVal("ami-BEFORE"),
  2374  				"root_block_device": cty.ListVal([]cty.Value{
  2375  					cty.ObjectVal(map[string]cty.Value{
  2376  						"volume_type": cty.StringVal("gp2"),
  2377  					}),
  2378  				}),
  2379  				"disks": cty.ListVal([]cty.Value{
  2380  					cty.ObjectVal(map[string]cty.Value{
  2381  						"mount_point": cty.StringVal("/var/diska"),
  2382  						"size":        cty.StringVal("50GB"),
  2383  					}),
  2384  				}),
  2385  			}),
  2386  			After: cty.ObjectVal(map[string]cty.Value{
  2387  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2388  				"ami": cty.StringVal("ami-AFTER"),
  2389  				"root_block_device": cty.ListVal([]cty.Value{
  2390  					cty.ObjectVal(map[string]cty.Value{
  2391  						"volume_type": cty.StringVal("gp2"),
  2392  					}),
  2393  				}),
  2394  				"disks": cty.ListVal([]cty.Value{
  2395  					cty.ObjectVal(map[string]cty.Value{
  2396  						"mount_point": cty.StringVal("/var/diska"),
  2397  						"size":        cty.StringVal("50GB"),
  2398  					}),
  2399  				}),
  2400  			}),
  2401  			RequiredReplace: cty.NewPathSet(),
  2402  			Schema:          testSchema(configschema.NestingList),
  2403  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2404    ~ resource "test_instance" "example" {
  2405        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2406          id    = "i-02ae66f368e8518a9"
  2407          # (1 unchanged attribute hidden)
  2408  
  2409          # (1 unchanged block hidden)
  2410      }
  2411  `,
  2412  		},
  2413  		"in-place update - creation": {
  2414  			Action: plans.Update,
  2415  			Mode:   addrs.ManagedResourceMode,
  2416  			Before: cty.ObjectVal(map[string]cty.Value{
  2417  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2418  				"ami": cty.StringVal("ami-BEFORE"),
  2419  				"root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  2420  					"volume_type": cty.String,
  2421  				})),
  2422  				"disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  2423  					"mount_point": cty.String,
  2424  					"size":        cty.String,
  2425  				})),
  2426  			}),
  2427  			After: cty.ObjectVal(map[string]cty.Value{
  2428  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2429  				"ami": cty.StringVal("ami-AFTER"),
  2430  				"disks": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
  2431  					"mount_point": cty.StringVal("/var/diska"),
  2432  					"size":        cty.StringVal("50GB"),
  2433  				})}),
  2434  				"root_block_device": cty.ListVal([]cty.Value{
  2435  					cty.ObjectVal(map[string]cty.Value{
  2436  						"volume_type": cty.NullVal(cty.String),
  2437  					}),
  2438  				}),
  2439  			}),
  2440  			RequiredReplace: cty.NewPathSet(),
  2441  			Schema:          testSchema(configschema.NestingList),
  2442  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2443    ~ resource "test_instance" "example" {
  2444        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2445        ~ disks = [
  2446            + {
  2447                + mount_point = "/var/diska"
  2448                + size        = "50GB"
  2449              },
  2450          ]
  2451          id    = "i-02ae66f368e8518a9"
  2452  
  2453        + root_block_device {}
  2454      }
  2455  `,
  2456  		},
  2457  		"in-place update - first insertion": {
  2458  			Action: plans.Update,
  2459  			Mode:   addrs.ManagedResourceMode,
  2460  			Before: cty.ObjectVal(map[string]cty.Value{
  2461  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2462  				"ami": cty.StringVal("ami-BEFORE"),
  2463  				"root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  2464  					"volume_type": cty.String,
  2465  				})),
  2466  				"disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  2467  					"mount_point": cty.String,
  2468  					"size":        cty.String,
  2469  				})),
  2470  			}),
  2471  			After: cty.ObjectVal(map[string]cty.Value{
  2472  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2473  				"ami": cty.StringVal("ami-AFTER"),
  2474  				"disks": cty.ListVal([]cty.Value{
  2475  					cty.ObjectVal(map[string]cty.Value{
  2476  						"mount_point": cty.StringVal("/var/diska"),
  2477  						"size":        cty.NullVal(cty.String),
  2478  					}),
  2479  				}),
  2480  				"root_block_device": cty.ListVal([]cty.Value{
  2481  					cty.ObjectVal(map[string]cty.Value{
  2482  						"volume_type": cty.StringVal("gp2"),
  2483  					}),
  2484  				}),
  2485  			}),
  2486  			RequiredReplace: cty.NewPathSet(),
  2487  			Schema:          testSchema(configschema.NestingList),
  2488  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2489    ~ resource "test_instance" "example" {
  2490        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2491        ~ disks = [
  2492            + {
  2493                + mount_point = "/var/diska"
  2494              },
  2495          ]
  2496          id    = "i-02ae66f368e8518a9"
  2497  
  2498        + root_block_device {
  2499            + volume_type = "gp2"
  2500          }
  2501      }
  2502  `,
  2503  		},
  2504  		"in-place update - insertion": {
  2505  			Action: plans.Update,
  2506  			Mode:   addrs.ManagedResourceMode,
  2507  			Before: cty.ObjectVal(map[string]cty.Value{
  2508  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2509  				"ami": cty.StringVal("ami-BEFORE"),
  2510  				"disks": cty.ListVal([]cty.Value{
  2511  					cty.ObjectVal(map[string]cty.Value{
  2512  						"mount_point": cty.StringVal("/var/diska"),
  2513  						"size":        cty.NullVal(cty.String),
  2514  					}),
  2515  					cty.ObjectVal(map[string]cty.Value{
  2516  						"mount_point": cty.StringVal("/var/diskb"),
  2517  						"size":        cty.StringVal("50GB"),
  2518  					}),
  2519  				}),
  2520  				"root_block_device": cty.ListVal([]cty.Value{
  2521  					cty.ObjectVal(map[string]cty.Value{
  2522  						"volume_type": cty.StringVal("gp2"),
  2523  						"new_field":   cty.NullVal(cty.String),
  2524  					}),
  2525  				}),
  2526  			}),
  2527  			After: cty.ObjectVal(map[string]cty.Value{
  2528  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2529  				"ami": cty.StringVal("ami-AFTER"),
  2530  				"disks": cty.ListVal([]cty.Value{
  2531  					cty.ObjectVal(map[string]cty.Value{
  2532  						"mount_point": cty.StringVal("/var/diska"),
  2533  						"size":        cty.StringVal("50GB"),
  2534  					}),
  2535  					cty.ObjectVal(map[string]cty.Value{
  2536  						"mount_point": cty.StringVal("/var/diskb"),
  2537  						"size":        cty.StringVal("50GB"),
  2538  					}),
  2539  				}),
  2540  				"root_block_device": cty.ListVal([]cty.Value{
  2541  					cty.ObjectVal(map[string]cty.Value{
  2542  						"volume_type": cty.StringVal("gp2"),
  2543  						"new_field":   cty.StringVal("new_value"),
  2544  					}),
  2545  				}),
  2546  			}),
  2547  			RequiredReplace: cty.NewPathSet(),
  2548  			Schema:          testSchemaPlus(configschema.NestingList),
  2549  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2550    ~ resource "test_instance" "example" {
  2551        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2552        ~ disks = [
  2553            ~ {
  2554                + size        = "50GB"
  2555                  # (1 unchanged attribute hidden)
  2556              },
  2557              # (1 unchanged element hidden)
  2558          ]
  2559          id    = "i-02ae66f368e8518a9"
  2560  
  2561        ~ root_block_device {
  2562            + new_field   = "new_value"
  2563              # (1 unchanged attribute hidden)
  2564          }
  2565      }
  2566  `,
  2567  		},
  2568  		"force-new update (inside blocks)": {
  2569  			Action:       plans.DeleteThenCreate,
  2570  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  2571  			Mode:         addrs.ManagedResourceMode,
  2572  			Before: cty.ObjectVal(map[string]cty.Value{
  2573  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2574  				"ami": cty.StringVal("ami-BEFORE"),
  2575  				"disks": cty.ListVal([]cty.Value{
  2576  					cty.ObjectVal(map[string]cty.Value{
  2577  						"mount_point": cty.StringVal("/var/diska"),
  2578  						"size":        cty.StringVal("50GB"),
  2579  					}),
  2580  				}),
  2581  				"root_block_device": cty.ListVal([]cty.Value{
  2582  					cty.ObjectVal(map[string]cty.Value{
  2583  						"volume_type": cty.StringVal("gp2"),
  2584  					}),
  2585  				}),
  2586  			}),
  2587  			After: cty.ObjectVal(map[string]cty.Value{
  2588  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2589  				"ami": cty.StringVal("ami-AFTER"),
  2590  				"disks": cty.ListVal([]cty.Value{
  2591  					cty.ObjectVal(map[string]cty.Value{
  2592  						"mount_point": cty.StringVal("/var/diskb"),
  2593  						"size":        cty.StringVal("50GB"),
  2594  					}),
  2595  				}),
  2596  				"root_block_device": cty.ListVal([]cty.Value{
  2597  					cty.ObjectVal(map[string]cty.Value{
  2598  						"volume_type": cty.StringVal("different"),
  2599  					}),
  2600  				}),
  2601  			}),
  2602  			RequiredReplace: cty.NewPathSet(
  2603  				cty.Path{
  2604  					cty.GetAttrStep{Name: "root_block_device"},
  2605  					cty.IndexStep{Key: cty.NumberIntVal(0)},
  2606  					cty.GetAttrStep{Name: "volume_type"},
  2607  				},
  2608  				cty.Path{
  2609  					cty.GetAttrStep{Name: "disks"},
  2610  					cty.IndexStep{Key: cty.NumberIntVal(0)},
  2611  					cty.GetAttrStep{Name: "mount_point"},
  2612  				},
  2613  			),
  2614  			Schema: testSchema(configschema.NestingList),
  2615  			ExpectedOutput: `  # test_instance.example must be replaced
  2616  -/+ resource "test_instance" "example" {
  2617        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2618        ~ disks = [
  2619            ~ {
  2620                ~ mount_point = "/var/diska" -> "/var/diskb" # forces replacement
  2621                  # (1 unchanged attribute hidden)
  2622              },
  2623          ]
  2624          id    = "i-02ae66f368e8518a9"
  2625  
  2626        ~ root_block_device {
  2627            ~ volume_type = "gp2" -> "different" # forces replacement
  2628          }
  2629      }
  2630  `,
  2631  		},
  2632  		"force-new update (whole block)": {
  2633  			Action:       plans.DeleteThenCreate,
  2634  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  2635  			Mode:         addrs.ManagedResourceMode,
  2636  			Before: cty.ObjectVal(map[string]cty.Value{
  2637  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2638  				"ami": cty.StringVal("ami-BEFORE"),
  2639  				"disks": cty.ListVal([]cty.Value{
  2640  					cty.ObjectVal(map[string]cty.Value{
  2641  						"mount_point": cty.StringVal("/var/diska"),
  2642  						"size":        cty.StringVal("50GB"),
  2643  					}),
  2644  				}),
  2645  				"root_block_device": cty.ListVal([]cty.Value{
  2646  					cty.ObjectVal(map[string]cty.Value{
  2647  						"volume_type": cty.StringVal("gp2"),
  2648  					}),
  2649  				}),
  2650  			}),
  2651  			After: cty.ObjectVal(map[string]cty.Value{
  2652  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2653  				"ami": cty.StringVal("ami-AFTER"),
  2654  				"disks": cty.ListVal([]cty.Value{
  2655  					cty.ObjectVal(map[string]cty.Value{
  2656  						"mount_point": cty.StringVal("/var/diskb"),
  2657  						"size":        cty.StringVal("50GB"),
  2658  					}),
  2659  				}),
  2660  				"root_block_device": cty.ListVal([]cty.Value{
  2661  					cty.ObjectVal(map[string]cty.Value{
  2662  						"volume_type": cty.StringVal("different"),
  2663  					}),
  2664  				}),
  2665  			}),
  2666  			RequiredReplace: cty.NewPathSet(
  2667  				cty.Path{cty.GetAttrStep{Name: "root_block_device"}},
  2668  				cty.Path{cty.GetAttrStep{Name: "disks"}},
  2669  			),
  2670  			Schema: testSchema(configschema.NestingList),
  2671  			ExpectedOutput: `  # test_instance.example must be replaced
  2672  -/+ resource "test_instance" "example" {
  2673        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2674        ~ disks = [ # forces replacement
  2675            ~ {
  2676                ~ mount_point = "/var/diska" -> "/var/diskb"
  2677                  # (1 unchanged attribute hidden)
  2678              },
  2679          ]
  2680          id    = "i-02ae66f368e8518a9"
  2681  
  2682        ~ root_block_device { # forces replacement
  2683            ~ volume_type = "gp2" -> "different"
  2684          }
  2685      }
  2686  `,
  2687  		},
  2688  		"in-place update - deletion": {
  2689  			Action: plans.Update,
  2690  			Mode:   addrs.ManagedResourceMode,
  2691  			Before: cty.ObjectVal(map[string]cty.Value{
  2692  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2693  				"ami": cty.StringVal("ami-BEFORE"),
  2694  				"disks": cty.ListVal([]cty.Value{
  2695  					cty.ObjectVal(map[string]cty.Value{
  2696  						"mount_point": cty.StringVal("/var/diska"),
  2697  						"size":        cty.StringVal("50GB"),
  2698  					}),
  2699  				}),
  2700  				"root_block_device": cty.ListVal([]cty.Value{
  2701  					cty.ObjectVal(map[string]cty.Value{
  2702  						"volume_type": cty.StringVal("gp2"),
  2703  					}),
  2704  				}),
  2705  			}),
  2706  			After: cty.ObjectVal(map[string]cty.Value{
  2707  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2708  				"ami": cty.StringVal("ami-AFTER"),
  2709  				"disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  2710  					"mount_point": cty.String,
  2711  					"size":        cty.String,
  2712  				})),
  2713  				"root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  2714  					"volume_type": cty.String,
  2715  				})),
  2716  			}),
  2717  			RequiredReplace: cty.NewPathSet(),
  2718  			Schema:          testSchema(configschema.NestingList),
  2719  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2720    ~ resource "test_instance" "example" {
  2721        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2722        ~ disks = [
  2723            - {
  2724                - mount_point = "/var/diska" -> null
  2725                - size        = "50GB" -> null
  2726              },
  2727          ]
  2728          id    = "i-02ae66f368e8518a9"
  2729  
  2730        - root_block_device {
  2731            - volume_type = "gp2" -> null
  2732          }
  2733      }
  2734  `,
  2735  		},
  2736  		"with dynamically-typed attribute": {
  2737  			Action: plans.Update,
  2738  			Mode:   addrs.ManagedResourceMode,
  2739  			Before: cty.ObjectVal(map[string]cty.Value{
  2740  				"block": cty.EmptyTupleVal,
  2741  			}),
  2742  			After: cty.ObjectVal(map[string]cty.Value{
  2743  				"block": cty.TupleVal([]cty.Value{
  2744  					cty.ObjectVal(map[string]cty.Value{
  2745  						"attr": cty.StringVal("foo"),
  2746  					}),
  2747  					cty.ObjectVal(map[string]cty.Value{
  2748  						"attr": cty.True,
  2749  					}),
  2750  				}),
  2751  			}),
  2752  			RequiredReplace: cty.NewPathSet(),
  2753  			Schema: &configschema.Block{
  2754  				BlockTypes: map[string]*configschema.NestedBlock{
  2755  					"block": {
  2756  						Block: configschema.Block{
  2757  							Attributes: map[string]*configschema.Attribute{
  2758  								"attr": {Type: cty.DynamicPseudoType, Optional: true},
  2759  							},
  2760  						},
  2761  						Nesting: configschema.NestingList,
  2762  					},
  2763  				},
  2764  			},
  2765  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2766    ~ resource "test_instance" "example" {
  2767        + block {
  2768            + attr = "foo"
  2769          }
  2770        + block {
  2771            + attr = true
  2772          }
  2773      }
  2774  `,
  2775  		},
  2776  		"in-place sequence update - deletion": {
  2777  			Action: plans.Update,
  2778  			Mode:   addrs.ManagedResourceMode,
  2779  			Before: cty.ObjectVal(map[string]cty.Value{
  2780  				"list": cty.ListVal([]cty.Value{
  2781  					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("x")}),
  2782  					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}),
  2783  				}),
  2784  			}),
  2785  			After: cty.ObjectVal(map[string]cty.Value{
  2786  				"list": cty.ListVal([]cty.Value{
  2787  					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}),
  2788  					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("z")}),
  2789  				}),
  2790  			}),
  2791  			RequiredReplace: cty.NewPathSet(),
  2792  			Schema: &configschema.Block{
  2793  				BlockTypes: map[string]*configschema.NestedBlock{
  2794  					"list": {
  2795  						Block: configschema.Block{
  2796  							Attributes: map[string]*configschema.Attribute{
  2797  								"attr": {
  2798  									Type:     cty.String,
  2799  									Required: true,
  2800  								},
  2801  							},
  2802  						},
  2803  						Nesting: configschema.NestingList,
  2804  					},
  2805  				},
  2806  			},
  2807  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2808    ~ resource "test_instance" "example" {
  2809        ~ list {
  2810            ~ attr = "x" -> "y"
  2811          }
  2812        ~ list {
  2813            ~ attr = "y" -> "z"
  2814          }
  2815      }
  2816  `,
  2817  		},
  2818  		"in-place update - unknown": {
  2819  			Action: plans.Update,
  2820  			Mode:   addrs.ManagedResourceMode,
  2821  			Before: cty.ObjectVal(map[string]cty.Value{
  2822  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2823  				"ami": cty.StringVal("ami-BEFORE"),
  2824  				"disks": cty.ListVal([]cty.Value{
  2825  					cty.ObjectVal(map[string]cty.Value{
  2826  						"mount_point": cty.StringVal("/var/diska"),
  2827  						"size":        cty.StringVal("50GB"),
  2828  					}),
  2829  				}),
  2830  				"root_block_device": cty.ListVal([]cty.Value{
  2831  					cty.ObjectVal(map[string]cty.Value{
  2832  						"volume_type": cty.StringVal("gp2"),
  2833  						"new_field":   cty.StringVal("new_value"),
  2834  					}),
  2835  				}),
  2836  			}),
  2837  			After: cty.ObjectVal(map[string]cty.Value{
  2838  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2839  				"ami": cty.StringVal("ami-AFTER"),
  2840  				"disks": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
  2841  					"mount_point": cty.String,
  2842  					"size":        cty.String,
  2843  				}))),
  2844  				"root_block_device": cty.ListVal([]cty.Value{
  2845  					cty.ObjectVal(map[string]cty.Value{
  2846  						"volume_type": cty.StringVal("gp2"),
  2847  						"new_field":   cty.StringVal("new_value"),
  2848  					}),
  2849  				}),
  2850  			}),
  2851  			RequiredReplace: cty.NewPathSet(),
  2852  			Schema:          testSchemaPlus(configschema.NestingList),
  2853  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2854    ~ resource "test_instance" "example" {
  2855        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2856        ~ disks = [
  2857            - {
  2858                - mount_point = "/var/diska" -> null
  2859                - size        = "50GB" -> null
  2860              },
  2861          ] -> (known after apply)
  2862          id    = "i-02ae66f368e8518a9"
  2863  
  2864          # (1 unchanged block hidden)
  2865      }
  2866  `,
  2867  		},
  2868  		"in-place update - modification": {
  2869  			Action: plans.Update,
  2870  			Mode:   addrs.ManagedResourceMode,
  2871  			Before: cty.ObjectVal(map[string]cty.Value{
  2872  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2873  				"ami": cty.StringVal("ami-BEFORE"),
  2874  				"disks": cty.ListVal([]cty.Value{
  2875  					cty.ObjectVal(map[string]cty.Value{
  2876  						"mount_point": cty.StringVal("/var/diska"),
  2877  						"size":        cty.StringVal("50GB"),
  2878  					}),
  2879  					cty.ObjectVal(map[string]cty.Value{
  2880  						"mount_point": cty.StringVal("/var/diskb"),
  2881  						"size":        cty.StringVal("50GB"),
  2882  					}),
  2883  					cty.ObjectVal(map[string]cty.Value{
  2884  						"mount_point": cty.StringVal("/var/diskc"),
  2885  						"size":        cty.StringVal("50GB"),
  2886  					}),
  2887  				}),
  2888  				"root_block_device": cty.ListVal([]cty.Value{
  2889  					cty.ObjectVal(map[string]cty.Value{
  2890  						"volume_type": cty.StringVal("gp2"),
  2891  						"new_field":   cty.StringVal("new_value"),
  2892  					}),
  2893  				}),
  2894  			}),
  2895  			After: cty.ObjectVal(map[string]cty.Value{
  2896  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2897  				"ami": cty.StringVal("ami-AFTER"),
  2898  				"disks": cty.ListVal([]cty.Value{
  2899  					cty.ObjectVal(map[string]cty.Value{
  2900  						"mount_point": cty.StringVal("/var/diska"),
  2901  						"size":        cty.StringVal("50GB"),
  2902  					}),
  2903  					cty.ObjectVal(map[string]cty.Value{
  2904  						"mount_point": cty.StringVal("/var/diskb"),
  2905  						"size":        cty.StringVal("75GB"),
  2906  					}),
  2907  					cty.ObjectVal(map[string]cty.Value{
  2908  						"mount_point": cty.StringVal("/var/diskc"),
  2909  						"size":        cty.StringVal("25GB"),
  2910  					}),
  2911  				}),
  2912  				"root_block_device": cty.ListVal([]cty.Value{
  2913  					cty.ObjectVal(map[string]cty.Value{
  2914  						"volume_type": cty.StringVal("gp2"),
  2915  						"new_field":   cty.StringVal("new_value"),
  2916  					}),
  2917  				}),
  2918  			}),
  2919  			RequiredReplace: cty.NewPathSet(),
  2920  			Schema:          testSchemaPlus(configschema.NestingList),
  2921  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2922    ~ resource "test_instance" "example" {
  2923        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2924        ~ disks = [
  2925            ~ {
  2926                ~ size        = "50GB" -> "75GB"
  2927                  # (1 unchanged attribute hidden)
  2928              },
  2929            ~ {
  2930                ~ size        = "50GB" -> "25GB"
  2931                  # (1 unchanged attribute hidden)
  2932              },
  2933              # (1 unchanged element hidden)
  2934          ]
  2935          id    = "i-02ae66f368e8518a9"
  2936  
  2937          # (1 unchanged block hidden)
  2938      }
  2939  `,
  2940  		},
  2941  	}
  2942  	runTestCases(t, testCases)
  2943  }
  2944  
  2945  func TestResourceChange_nestedSet(t *testing.T) {
  2946  	testCases := map[string]testCase{
  2947  		"creation from null - sensitive set": {
  2948  			Action: plans.Create,
  2949  			Mode:   addrs.ManagedResourceMode,
  2950  			Before: cty.NullVal(cty.Object(map[string]cty.Type{
  2951  				"id":  cty.String,
  2952  				"ami": cty.String,
  2953  				"disks": cty.Set(cty.Object(map[string]cty.Type{
  2954  					"mount_point": cty.String,
  2955  					"size":        cty.String,
  2956  				})),
  2957  				"root_block_device": cty.Set(cty.Object(map[string]cty.Type{
  2958  					"volume_type": cty.String,
  2959  				})),
  2960  			})),
  2961  			After: cty.ObjectVal(map[string]cty.Value{
  2962  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2963  				"ami": cty.StringVal("ami-AFTER"),
  2964  				"disks": cty.SetVal([]cty.Value{
  2965  					cty.ObjectVal(map[string]cty.Value{
  2966  						"mount_point": cty.StringVal("/var/diska"),
  2967  						"size":        cty.NullVal(cty.String),
  2968  					}),
  2969  				}),
  2970  				"root_block_device": cty.SetVal([]cty.Value{
  2971  					cty.ObjectVal(map[string]cty.Value{
  2972  						"volume_type": cty.StringVal("gp2"),
  2973  					}),
  2974  				}),
  2975  			}),
  2976  			AfterValMarks: []cty.PathValueMarks{
  2977  				{
  2978  					Path:  cty.Path{cty.GetAttrStep{Name: "disks"}},
  2979  					Marks: cty.NewValueMarks(marks.Sensitive),
  2980  				},
  2981  			},
  2982  			RequiredReplace: cty.NewPathSet(),
  2983  			Schema:          testSchema(configschema.NestingSet),
  2984  			ExpectedOutput: `  # test_instance.example will be created
  2985    + resource "test_instance" "example" {
  2986        + ami   = "ami-AFTER"
  2987        + disks = (sensitive value)
  2988        + id    = "i-02ae66f368e8518a9"
  2989  
  2990        + root_block_device {
  2991            + volume_type = "gp2"
  2992          }
  2993      }
  2994  `,
  2995  		},
  2996  		"in-place update - creation": {
  2997  			Action: plans.Update,
  2998  			Mode:   addrs.ManagedResourceMode,
  2999  			Before: cty.ObjectVal(map[string]cty.Value{
  3000  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3001  				"ami": cty.StringVal("ami-BEFORE"),
  3002  				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3003  					"mount_point": cty.String,
  3004  					"size":        cty.String,
  3005  				})),
  3006  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3007  					"volume_type": cty.String,
  3008  				})),
  3009  			}),
  3010  			After: cty.ObjectVal(map[string]cty.Value{
  3011  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3012  				"ami": cty.StringVal("ami-AFTER"),
  3013  				"disks": cty.SetVal([]cty.Value{
  3014  					cty.ObjectVal(map[string]cty.Value{
  3015  						"mount_point": cty.StringVal("/var/diska"),
  3016  						"size":        cty.NullVal(cty.String),
  3017  					}),
  3018  				}),
  3019  				"root_block_device": cty.SetVal([]cty.Value{
  3020  					cty.ObjectVal(map[string]cty.Value{
  3021  						"volume_type": cty.StringVal("gp2"),
  3022  					}),
  3023  				}),
  3024  			}),
  3025  			RequiredReplace: cty.NewPathSet(),
  3026  			Schema:          testSchema(configschema.NestingSet),
  3027  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3028    ~ resource "test_instance" "example" {
  3029        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3030        ~ disks = [
  3031            + {
  3032                + mount_point = "/var/diska"
  3033              },
  3034          ]
  3035          id    = "i-02ae66f368e8518a9"
  3036  
  3037        + root_block_device {
  3038            + volume_type = "gp2"
  3039          }
  3040      }
  3041  `,
  3042  		},
  3043  		"in-place update - creation - sensitive set": {
  3044  			Action: plans.Update,
  3045  			Mode:   addrs.ManagedResourceMode,
  3046  			Before: cty.ObjectVal(map[string]cty.Value{
  3047  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3048  				"ami": cty.StringVal("ami-BEFORE"),
  3049  				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3050  					"mount_point": cty.String,
  3051  					"size":        cty.String,
  3052  				})),
  3053  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3054  					"volume_type": cty.String,
  3055  				})),
  3056  			}),
  3057  			After: cty.ObjectVal(map[string]cty.Value{
  3058  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3059  				"ami": cty.StringVal("ami-AFTER"),
  3060  				"disks": cty.SetVal([]cty.Value{
  3061  					cty.ObjectVal(map[string]cty.Value{
  3062  						"mount_point": cty.StringVal("/var/diska"),
  3063  						"size":        cty.NullVal(cty.String),
  3064  					}),
  3065  				}),
  3066  				"root_block_device": cty.SetVal([]cty.Value{
  3067  					cty.ObjectVal(map[string]cty.Value{
  3068  						"volume_type": cty.StringVal("gp2"),
  3069  					}),
  3070  				}),
  3071  			}),
  3072  			AfterValMarks: []cty.PathValueMarks{
  3073  				{
  3074  					Path:  cty.Path{cty.GetAttrStep{Name: "disks"}},
  3075  					Marks: cty.NewValueMarks(marks.Sensitive),
  3076  				},
  3077  			},
  3078  			RequiredReplace: cty.NewPathSet(),
  3079  			Schema:          testSchema(configschema.NestingSet),
  3080  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3081    ~ resource "test_instance" "example" {
  3082        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3083        # Warning: this attribute value will be marked as sensitive and will not
  3084        # display in UI output after applying this change.
  3085        ~ disks = (sensitive value)
  3086          id    = "i-02ae66f368e8518a9"
  3087  
  3088        + root_block_device {
  3089            + volume_type = "gp2"
  3090          }
  3091      }
  3092  `,
  3093  		},
  3094  		"in-place update - marking set sensitive": {
  3095  			Action: plans.Update,
  3096  			Mode:   addrs.ManagedResourceMode,
  3097  			Before: cty.ObjectVal(map[string]cty.Value{
  3098  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3099  				"ami": cty.StringVal("ami-BEFORE"),
  3100  				"disks": cty.SetVal([]cty.Value{
  3101  					cty.ObjectVal(map[string]cty.Value{
  3102  						"mount_point": cty.StringVal("/var/diska"),
  3103  						"size":        cty.StringVal("50GB"),
  3104  					}),
  3105  				}),
  3106  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3107  					"volume_type": cty.String,
  3108  				})),
  3109  			}),
  3110  			After: cty.ObjectVal(map[string]cty.Value{
  3111  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3112  				"ami": cty.StringVal("ami-AFTER"),
  3113  				"disks": cty.SetVal([]cty.Value{
  3114  					cty.ObjectVal(map[string]cty.Value{
  3115  						"mount_point": cty.StringVal("/var/diska"),
  3116  						"size":        cty.StringVal("50GB"),
  3117  					}),
  3118  				}),
  3119  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3120  					"volume_type": cty.String,
  3121  				})),
  3122  			}),
  3123  			AfterValMarks: []cty.PathValueMarks{
  3124  				{
  3125  					Path:  cty.Path{cty.GetAttrStep{Name: "disks"}},
  3126  					Marks: cty.NewValueMarks(marks.Sensitive),
  3127  				},
  3128  			},
  3129  			RequiredReplace: cty.NewPathSet(),
  3130  			Schema:          testSchema(configschema.NestingSet),
  3131  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3132    ~ resource "test_instance" "example" {
  3133        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3134        # Warning: this attribute value will be marked as sensitive and will not
  3135        # display in UI output after applying this change. The value is unchanged.
  3136        ~ disks = (sensitive value)
  3137          id    = "i-02ae66f368e8518a9"
  3138      }
  3139  `,
  3140  		},
  3141  		"in-place update - insertion": {
  3142  			Action: plans.Update,
  3143  			Mode:   addrs.ManagedResourceMode,
  3144  			Before: cty.ObjectVal(map[string]cty.Value{
  3145  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3146  				"ami": cty.StringVal("ami-BEFORE"),
  3147  				"disks": cty.SetVal([]cty.Value{
  3148  					cty.ObjectVal(map[string]cty.Value{
  3149  						"mount_point": cty.StringVal("/var/diska"),
  3150  						"size":        cty.NullVal(cty.String),
  3151  					}),
  3152  					cty.ObjectVal(map[string]cty.Value{
  3153  						"mount_point": cty.StringVal("/var/diskb"),
  3154  						"size":        cty.StringVal("100GB"),
  3155  					}),
  3156  				}),
  3157  				"root_block_device": cty.SetVal([]cty.Value{
  3158  					cty.ObjectVal(map[string]cty.Value{
  3159  						"volume_type": cty.StringVal("gp2"),
  3160  						"new_field":   cty.NullVal(cty.String),
  3161  					}),
  3162  				}),
  3163  			}),
  3164  			After: cty.ObjectVal(map[string]cty.Value{
  3165  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3166  				"ami": cty.StringVal("ami-AFTER"),
  3167  				"disks": cty.SetVal([]cty.Value{
  3168  					cty.ObjectVal(map[string]cty.Value{
  3169  						"mount_point": cty.StringVal("/var/diska"),
  3170  						"size":        cty.StringVal("50GB"),
  3171  					}),
  3172  					cty.ObjectVal(map[string]cty.Value{
  3173  						"mount_point": cty.StringVal("/var/diskb"),
  3174  						"size":        cty.StringVal("100GB"),
  3175  					}),
  3176  				}),
  3177  				"root_block_device": cty.SetVal([]cty.Value{
  3178  					cty.ObjectVal(map[string]cty.Value{
  3179  						"volume_type": cty.StringVal("gp2"),
  3180  						"new_field":   cty.StringVal("new_value"),
  3181  					}),
  3182  				}),
  3183  			}),
  3184  			RequiredReplace: cty.NewPathSet(),
  3185  			Schema:          testSchemaPlus(configschema.NestingSet),
  3186  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3187    ~ resource "test_instance" "example" {
  3188        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3189        ~ disks = [
  3190            + {
  3191                + mount_point = "/var/diska"
  3192                + size        = "50GB"
  3193              },
  3194            - {
  3195                - mount_point = "/var/diska" -> null
  3196              },
  3197              # (1 unchanged element hidden)
  3198          ]
  3199          id    = "i-02ae66f368e8518a9"
  3200  
  3201        + root_block_device {
  3202            + new_field   = "new_value"
  3203            + volume_type = "gp2"
  3204          }
  3205        - root_block_device {
  3206            - volume_type = "gp2" -> null
  3207          }
  3208      }
  3209  `,
  3210  		},
  3211  		"force-new update (whole block)": {
  3212  			Action:       plans.DeleteThenCreate,
  3213  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  3214  			Mode:         addrs.ManagedResourceMode,
  3215  			Before: cty.ObjectVal(map[string]cty.Value{
  3216  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3217  				"ami": cty.StringVal("ami-BEFORE"),
  3218  				"root_block_device": cty.SetVal([]cty.Value{
  3219  					cty.ObjectVal(map[string]cty.Value{
  3220  						"volume_type": cty.StringVal("gp2"),
  3221  					}),
  3222  				}),
  3223  				"disks": cty.SetVal([]cty.Value{
  3224  					cty.ObjectVal(map[string]cty.Value{
  3225  						"mount_point": cty.StringVal("/var/diska"),
  3226  						"size":        cty.StringVal("50GB"),
  3227  					}),
  3228  				}),
  3229  			}),
  3230  			After: cty.ObjectVal(map[string]cty.Value{
  3231  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3232  				"ami": cty.StringVal("ami-AFTER"),
  3233  				"root_block_device": cty.SetVal([]cty.Value{
  3234  					cty.ObjectVal(map[string]cty.Value{
  3235  						"volume_type": cty.StringVal("different"),
  3236  					}),
  3237  				}),
  3238  				"disks": cty.SetVal([]cty.Value{
  3239  					cty.ObjectVal(map[string]cty.Value{
  3240  						"mount_point": cty.StringVal("/var/diskb"),
  3241  						"size":        cty.StringVal("50GB"),
  3242  					}),
  3243  				}),
  3244  			}),
  3245  			RequiredReplace: cty.NewPathSet(
  3246  				cty.Path{cty.GetAttrStep{Name: "root_block_device"}},
  3247  				cty.Path{cty.GetAttrStep{Name: "disks"}},
  3248  			),
  3249  			Schema: testSchema(configschema.NestingSet),
  3250  			ExpectedOutput: `  # test_instance.example must be replaced
  3251  -/+ resource "test_instance" "example" {
  3252        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3253        ~ disks = [
  3254            - { # forces replacement
  3255                - mount_point = "/var/diska" -> null
  3256                - size        = "50GB" -> null
  3257              },
  3258            + { # forces replacement
  3259                + mount_point = "/var/diskb"
  3260                + size        = "50GB"
  3261              },
  3262          ]
  3263          id    = "i-02ae66f368e8518a9"
  3264  
  3265        + root_block_device { # forces replacement
  3266            + volume_type = "different"
  3267          }
  3268        - root_block_device { # forces replacement
  3269            - volume_type = "gp2" -> null
  3270          }
  3271      }
  3272  `,
  3273  		},
  3274  		"in-place update - deletion": {
  3275  			Action: plans.Update,
  3276  			Mode:   addrs.ManagedResourceMode,
  3277  			Before: cty.ObjectVal(map[string]cty.Value{
  3278  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3279  				"ami": cty.StringVal("ami-BEFORE"),
  3280  				"root_block_device": cty.SetVal([]cty.Value{
  3281  					cty.ObjectVal(map[string]cty.Value{
  3282  						"volume_type": cty.StringVal("gp2"),
  3283  						"new_field":   cty.StringVal("new_value"),
  3284  					}),
  3285  				}),
  3286  				"disks": cty.SetVal([]cty.Value{
  3287  					cty.ObjectVal(map[string]cty.Value{
  3288  						"mount_point": cty.StringVal("/var/diska"),
  3289  						"size":        cty.StringVal("50GB"),
  3290  					}),
  3291  				}),
  3292  			}),
  3293  			After: cty.ObjectVal(map[string]cty.Value{
  3294  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3295  				"ami": cty.StringVal("ami-AFTER"),
  3296  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3297  					"volume_type": cty.String,
  3298  					"new_field":   cty.String,
  3299  				})),
  3300  				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3301  					"mount_point": cty.String,
  3302  					"size":        cty.String,
  3303  				})),
  3304  			}),
  3305  			RequiredReplace: cty.NewPathSet(),
  3306  			Schema:          testSchemaPlus(configschema.NestingSet),
  3307  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3308    ~ resource "test_instance" "example" {
  3309        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3310        ~ disks = [
  3311            - {
  3312                - mount_point = "/var/diska" -> null
  3313                - size        = "50GB" -> null
  3314              },
  3315          ]
  3316          id    = "i-02ae66f368e8518a9"
  3317  
  3318        - root_block_device {
  3319            - new_field   = "new_value" -> null
  3320            - volume_type = "gp2" -> null
  3321          }
  3322      }
  3323  `,
  3324  		},
  3325  		"in-place update - empty nested sets": {
  3326  			Action: plans.Update,
  3327  			Mode:   addrs.ManagedResourceMode,
  3328  			Before: cty.ObjectVal(map[string]cty.Value{
  3329  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3330  				"ami": cty.StringVal("ami-BEFORE"),
  3331  				"disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
  3332  					"mount_point": cty.String,
  3333  					"size":        cty.String,
  3334  				}))),
  3335  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3336  					"volume_type": cty.String,
  3337  				})),
  3338  			}),
  3339  			After: cty.ObjectVal(map[string]cty.Value{
  3340  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3341  				"ami": cty.StringVal("ami-AFTER"),
  3342  				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3343  					"mount_point": cty.String,
  3344  					"size":        cty.String,
  3345  				})),
  3346  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  3347  					"volume_type": cty.String,
  3348  				})),
  3349  			}),
  3350  			RequiredReplace: cty.NewPathSet(),
  3351  			Schema:          testSchema(configschema.NestingSet),
  3352  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3353    ~ resource "test_instance" "example" {
  3354        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3355        + disks = [
  3356          ]
  3357          id    = "i-02ae66f368e8518a9"
  3358      }
  3359  `,
  3360  		},
  3361  		"in-place update - null insertion": {
  3362  			Action: plans.Update,
  3363  			Mode:   addrs.ManagedResourceMode,
  3364  			Before: cty.ObjectVal(map[string]cty.Value{
  3365  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3366  				"ami": cty.StringVal("ami-BEFORE"),
  3367  				"disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
  3368  					"mount_point": cty.String,
  3369  					"size":        cty.String,
  3370  				}))),
  3371  				"root_block_device": cty.SetVal([]cty.Value{
  3372  					cty.ObjectVal(map[string]cty.Value{
  3373  						"volume_type": cty.StringVal("gp2"),
  3374  						"new_field":   cty.NullVal(cty.String),
  3375  					}),
  3376  				}),
  3377  			}),
  3378  			After: cty.ObjectVal(map[string]cty.Value{
  3379  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3380  				"ami": cty.StringVal("ami-AFTER"),
  3381  				"disks": cty.SetVal([]cty.Value{
  3382  					cty.ObjectVal(map[string]cty.Value{
  3383  						"mount_point": cty.StringVal("/var/diska"),
  3384  						"size":        cty.StringVal("50GB"),
  3385  					}),
  3386  				}),
  3387  				"root_block_device": cty.SetVal([]cty.Value{
  3388  					cty.ObjectVal(map[string]cty.Value{
  3389  						"volume_type": cty.StringVal("gp2"),
  3390  						"new_field":   cty.StringVal("new_value"),
  3391  					}),
  3392  				}),
  3393  			}),
  3394  			RequiredReplace: cty.NewPathSet(),
  3395  			Schema:          testSchemaPlus(configschema.NestingSet),
  3396  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3397    ~ resource "test_instance" "example" {
  3398        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3399        + disks = [
  3400            + {
  3401                + mount_point = "/var/diska"
  3402                + size        = "50GB"
  3403              },
  3404          ]
  3405          id    = "i-02ae66f368e8518a9"
  3406  
  3407        + root_block_device {
  3408            + new_field   = "new_value"
  3409            + volume_type = "gp2"
  3410          }
  3411        - root_block_device {
  3412            - volume_type = "gp2" -> null
  3413          }
  3414      }
  3415  `,
  3416  		},
  3417  		"in-place update - unknown": {
  3418  			Action: plans.Update,
  3419  			Mode:   addrs.ManagedResourceMode,
  3420  			Before: cty.ObjectVal(map[string]cty.Value{
  3421  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3422  				"ami": cty.StringVal("ami-BEFORE"),
  3423  				"disks": cty.SetVal([]cty.Value{
  3424  					cty.ObjectVal(map[string]cty.Value{
  3425  						"mount_point": cty.StringVal("/var/diska"),
  3426  						"size":        cty.StringVal("50GB"),
  3427  					}),
  3428  				}),
  3429  				"root_block_device": cty.SetVal([]cty.Value{
  3430  					cty.ObjectVal(map[string]cty.Value{
  3431  						"volume_type": cty.StringVal("gp2"),
  3432  						"new_field":   cty.StringVal("new_value"),
  3433  					}),
  3434  				}),
  3435  			}),
  3436  			After: cty.ObjectVal(map[string]cty.Value{
  3437  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3438  				"ami": cty.StringVal("ami-AFTER"),
  3439  				"disks": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
  3440  					"mount_point": cty.String,
  3441  					"size":        cty.String,
  3442  				}))),
  3443  				"root_block_device": cty.SetVal([]cty.Value{
  3444  					cty.ObjectVal(map[string]cty.Value{
  3445  						"volume_type": cty.StringVal("gp2"),
  3446  						"new_field":   cty.StringVal("new_value"),
  3447  					}),
  3448  				}),
  3449  			}),
  3450  			RequiredReplace: cty.NewPathSet(),
  3451  			Schema:          testSchemaPlus(configschema.NestingSet),
  3452  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3453    ~ resource "test_instance" "example" {
  3454        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3455        ~ disks = [
  3456            - {
  3457                - mount_point = "/var/diska" -> null
  3458                - size        = "50GB" -> null
  3459              },
  3460          ] -> (known after apply)
  3461          id    = "i-02ae66f368e8518a9"
  3462  
  3463          # (1 unchanged block hidden)
  3464      }
  3465  `,
  3466  		},
  3467  	}
  3468  	runTestCases(t, testCases)
  3469  }
  3470  
  3471  func TestResourceChange_nestedMap(t *testing.T) {
  3472  	testCases := map[string]testCase{
  3473  		"creation from null": {
  3474  			Action: plans.Update,
  3475  			Mode:   addrs.ManagedResourceMode,
  3476  			Before: cty.ObjectVal(map[string]cty.Value{
  3477  				"id":  cty.NullVal(cty.String),
  3478  				"ami": cty.NullVal(cty.String),
  3479  				"disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
  3480  					"mount_point": cty.String,
  3481  					"size":        cty.String,
  3482  				}))),
  3483  				"root_block_device": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
  3484  					"volume_type": cty.String,
  3485  				}))),
  3486  			}),
  3487  			After: cty.ObjectVal(map[string]cty.Value{
  3488  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3489  				"ami": cty.StringVal("ami-AFTER"),
  3490  				"disks": cty.MapVal(map[string]cty.Value{
  3491  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3492  						"mount_point": cty.StringVal("/var/diska"),
  3493  						"size":        cty.NullVal(cty.String),
  3494  					}),
  3495  				}),
  3496  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3497  					"a": cty.ObjectVal(map[string]cty.Value{
  3498  						"volume_type": cty.StringVal("gp2"),
  3499  					}),
  3500  				}),
  3501  			}),
  3502  			RequiredReplace: cty.NewPathSet(),
  3503  			Schema:          testSchema(configschema.NestingMap),
  3504  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3505    ~ resource "test_instance" "example" {
  3506        + ami   = "ami-AFTER"
  3507        + disks = {
  3508            + "disk_a" = {
  3509                + mount_point = "/var/diska"
  3510              },
  3511          }
  3512        + id    = "i-02ae66f368e8518a9"
  3513  
  3514        + root_block_device "a" {
  3515            + volume_type = "gp2"
  3516          }
  3517      }
  3518  `,
  3519  		},
  3520  		"in-place update - creation": {
  3521  			Action: plans.Update,
  3522  			Mode:   addrs.ManagedResourceMode,
  3523  			Before: cty.ObjectVal(map[string]cty.Value{
  3524  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3525  				"ami": cty.StringVal("ami-BEFORE"),
  3526  				"disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  3527  					"mount_point": cty.String,
  3528  					"size":        cty.String,
  3529  				})),
  3530  				"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  3531  					"volume_type": cty.String,
  3532  				})),
  3533  			}),
  3534  			After: cty.ObjectVal(map[string]cty.Value{
  3535  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3536  				"ami": cty.StringVal("ami-AFTER"),
  3537  				"disks": cty.MapVal(map[string]cty.Value{
  3538  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3539  						"mount_point": cty.StringVal("/var/diska"),
  3540  						"size":        cty.NullVal(cty.String),
  3541  					}),
  3542  				}),
  3543  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3544  					"a": cty.ObjectVal(map[string]cty.Value{
  3545  						"volume_type": cty.StringVal("gp2"),
  3546  					}),
  3547  				}),
  3548  			}),
  3549  			RequiredReplace: cty.NewPathSet(),
  3550  			Schema:          testSchema(configschema.NestingMap),
  3551  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3552    ~ resource "test_instance" "example" {
  3553        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3554        ~ disks = {
  3555            + "disk_a" = {
  3556                + mount_point = "/var/diska"
  3557              },
  3558          }
  3559          id    = "i-02ae66f368e8518a9"
  3560  
  3561        + root_block_device "a" {
  3562            + volume_type = "gp2"
  3563          }
  3564      }
  3565  `,
  3566  		},
  3567  		"in-place update - change attr": {
  3568  			Action: plans.Update,
  3569  			Mode:   addrs.ManagedResourceMode,
  3570  			Before: cty.ObjectVal(map[string]cty.Value{
  3571  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3572  				"ami": cty.StringVal("ami-BEFORE"),
  3573  				"disks": cty.MapVal(map[string]cty.Value{
  3574  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3575  						"mount_point": cty.StringVal("/var/diska"),
  3576  						"size":        cty.NullVal(cty.String),
  3577  					}),
  3578  				}),
  3579  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3580  					"a": cty.ObjectVal(map[string]cty.Value{
  3581  						"volume_type": cty.StringVal("gp2"),
  3582  						"new_field":   cty.NullVal(cty.String),
  3583  					}),
  3584  				}),
  3585  			}),
  3586  			After: cty.ObjectVal(map[string]cty.Value{
  3587  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3588  				"ami": cty.StringVal("ami-AFTER"),
  3589  				"disks": cty.MapVal(map[string]cty.Value{
  3590  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3591  						"mount_point": cty.StringVal("/var/diska"),
  3592  						"size":        cty.StringVal("50GB"),
  3593  					}),
  3594  				}),
  3595  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3596  					"a": cty.ObjectVal(map[string]cty.Value{
  3597  						"volume_type": cty.StringVal("gp2"),
  3598  						"new_field":   cty.StringVal("new_value"),
  3599  					}),
  3600  				}),
  3601  			}),
  3602  			RequiredReplace: cty.NewPathSet(),
  3603  			Schema:          testSchemaPlus(configschema.NestingMap),
  3604  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3605    ~ resource "test_instance" "example" {
  3606        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3607        ~ disks = {
  3608            ~ "disk_a" = {
  3609                + size        = "50GB"
  3610                  # (1 unchanged attribute hidden)
  3611              },
  3612          }
  3613          id    = "i-02ae66f368e8518a9"
  3614  
  3615        ~ root_block_device "a" {
  3616            + new_field   = "new_value"
  3617              # (1 unchanged attribute hidden)
  3618          }
  3619      }
  3620  `,
  3621  		},
  3622  		"in-place update - insertion": {
  3623  			Action: plans.Update,
  3624  			Mode:   addrs.ManagedResourceMode,
  3625  			Before: cty.ObjectVal(map[string]cty.Value{
  3626  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3627  				"ami": cty.StringVal("ami-BEFORE"),
  3628  				"disks": cty.MapVal(map[string]cty.Value{
  3629  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3630  						"mount_point": cty.StringVal("/var/diska"),
  3631  						"size":        cty.StringVal("50GB"),
  3632  					}),
  3633  				}),
  3634  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3635  					"a": cty.ObjectVal(map[string]cty.Value{
  3636  						"volume_type": cty.StringVal("gp2"),
  3637  						"new_field":   cty.NullVal(cty.String),
  3638  					}),
  3639  				}),
  3640  			}),
  3641  			After: cty.ObjectVal(map[string]cty.Value{
  3642  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3643  				"ami": cty.StringVal("ami-AFTER"),
  3644  				"disks": cty.MapVal(map[string]cty.Value{
  3645  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3646  						"mount_point": cty.StringVal("/var/diska"),
  3647  						"size":        cty.StringVal("50GB"),
  3648  					}),
  3649  					"disk_2": cty.ObjectVal(map[string]cty.Value{
  3650  						"mount_point": cty.StringVal("/var/disk2"),
  3651  						"size":        cty.StringVal("50GB"),
  3652  					}),
  3653  				}),
  3654  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3655  					"a": cty.ObjectVal(map[string]cty.Value{
  3656  						"volume_type": cty.StringVal("gp2"),
  3657  						"new_field":   cty.NullVal(cty.String),
  3658  					}),
  3659  					"b": cty.ObjectVal(map[string]cty.Value{
  3660  						"volume_type": cty.StringVal("gp2"),
  3661  						"new_field":   cty.StringVal("new_value"),
  3662  					}),
  3663  				}),
  3664  			}),
  3665  			RequiredReplace: cty.NewPathSet(),
  3666  			Schema:          testSchemaPlus(configschema.NestingMap),
  3667  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3668    ~ resource "test_instance" "example" {
  3669        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3670        ~ disks = {
  3671            + "disk_2" = {
  3672                + mount_point = "/var/disk2"
  3673                + size        = "50GB"
  3674              },
  3675              # (1 unchanged element hidden)
  3676          }
  3677          id    = "i-02ae66f368e8518a9"
  3678  
  3679        + root_block_device "b" {
  3680            + new_field   = "new_value"
  3681            + volume_type = "gp2"
  3682          }
  3683          # (1 unchanged block hidden)
  3684      }
  3685  `,
  3686  		},
  3687  		"force-new update (whole block)": {
  3688  			Action:       plans.DeleteThenCreate,
  3689  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  3690  			Mode:         addrs.ManagedResourceMode,
  3691  			Before: cty.ObjectVal(map[string]cty.Value{
  3692  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3693  				"ami": cty.StringVal("ami-BEFORE"),
  3694  				"disks": cty.MapVal(map[string]cty.Value{
  3695  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3696  						"mount_point": cty.StringVal("/var/diska"),
  3697  						"size":        cty.StringVal("50GB"),
  3698  					}),
  3699  				}),
  3700  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3701  					"a": cty.ObjectVal(map[string]cty.Value{
  3702  						"volume_type": cty.StringVal("gp2"),
  3703  					}),
  3704  					"b": cty.ObjectVal(map[string]cty.Value{
  3705  						"volume_type": cty.StringVal("standard"),
  3706  					}),
  3707  				}),
  3708  			}),
  3709  			After: cty.ObjectVal(map[string]cty.Value{
  3710  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3711  				"ami": cty.StringVal("ami-AFTER"),
  3712  				"disks": cty.MapVal(map[string]cty.Value{
  3713  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3714  						"mount_point": cty.StringVal("/var/diska"),
  3715  						"size":        cty.StringVal("100GB"),
  3716  					}),
  3717  				}),
  3718  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3719  					"a": cty.ObjectVal(map[string]cty.Value{
  3720  						"volume_type": cty.StringVal("different"),
  3721  					}),
  3722  					"b": cty.ObjectVal(map[string]cty.Value{
  3723  						"volume_type": cty.StringVal("standard"),
  3724  					}),
  3725  				}),
  3726  			}),
  3727  			RequiredReplace: cty.NewPathSet(cty.Path{
  3728  				cty.GetAttrStep{Name: "root_block_device"},
  3729  				cty.IndexStep{Key: cty.StringVal("a")},
  3730  			},
  3731  				cty.Path{cty.GetAttrStep{Name: "disks"}},
  3732  			),
  3733  			Schema: testSchema(configschema.NestingMap),
  3734  			ExpectedOutput: `  # test_instance.example must be replaced
  3735  -/+ resource "test_instance" "example" {
  3736        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3737        ~ disks = {
  3738            ~ "disk_a" = { # forces replacement
  3739                ~ size        = "50GB" -> "100GB"
  3740                  # (1 unchanged attribute hidden)
  3741              },
  3742          }
  3743          id    = "i-02ae66f368e8518a9"
  3744  
  3745        ~ root_block_device "a" { # forces replacement
  3746            ~ volume_type = "gp2" -> "different"
  3747          }
  3748          # (1 unchanged block hidden)
  3749      }
  3750  `,
  3751  		},
  3752  		"in-place update - deletion": {
  3753  			Action: plans.Update,
  3754  			Mode:   addrs.ManagedResourceMode,
  3755  			Before: cty.ObjectVal(map[string]cty.Value{
  3756  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3757  				"ami": cty.StringVal("ami-BEFORE"),
  3758  				"disks": cty.MapVal(map[string]cty.Value{
  3759  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3760  						"mount_point": cty.StringVal("/var/diska"),
  3761  						"size":        cty.StringVal("50GB"),
  3762  					}),
  3763  				}),
  3764  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3765  					"a": cty.ObjectVal(map[string]cty.Value{
  3766  						"volume_type": cty.StringVal("gp2"),
  3767  						"new_field":   cty.StringVal("new_value"),
  3768  					}),
  3769  				}),
  3770  			}),
  3771  			After: cty.ObjectVal(map[string]cty.Value{
  3772  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3773  				"ami": cty.StringVal("ami-AFTER"),
  3774  				"disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  3775  					"mount_point": cty.String,
  3776  					"size":        cty.String,
  3777  				})),
  3778  				"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  3779  					"volume_type": cty.String,
  3780  					"new_field":   cty.String,
  3781  				})),
  3782  			}),
  3783  			RequiredReplace: cty.NewPathSet(),
  3784  			Schema:          testSchemaPlus(configschema.NestingMap),
  3785  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3786    ~ resource "test_instance" "example" {
  3787        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3788        ~ disks = {
  3789            - "disk_a" = {
  3790                - mount_point = "/var/diska" -> null
  3791                - size        = "50GB" -> null
  3792              },
  3793          }
  3794          id    = "i-02ae66f368e8518a9"
  3795  
  3796        - root_block_device "a" {
  3797            - new_field   = "new_value" -> null
  3798            - volume_type = "gp2" -> null
  3799          }
  3800      }
  3801  `,
  3802  		},
  3803  		"in-place update - unknown": {
  3804  			Action: plans.Update,
  3805  			Mode:   addrs.ManagedResourceMode,
  3806  			Before: cty.ObjectVal(map[string]cty.Value{
  3807  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3808  				"ami": cty.StringVal("ami-BEFORE"),
  3809  				"disks": cty.MapVal(map[string]cty.Value{
  3810  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3811  						"mount_point": cty.StringVal("/var/diska"),
  3812  						"size":        cty.StringVal("50GB"),
  3813  					}),
  3814  				}),
  3815  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3816  					"a": cty.ObjectVal(map[string]cty.Value{
  3817  						"volume_type": cty.StringVal("gp2"),
  3818  						"new_field":   cty.StringVal("new_value"),
  3819  					}),
  3820  				}),
  3821  			}),
  3822  			After: cty.ObjectVal(map[string]cty.Value{
  3823  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3824  				"ami": cty.StringVal("ami-AFTER"),
  3825  				"disks": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{
  3826  					"mount_point": cty.String,
  3827  					"size":        cty.String,
  3828  				}))),
  3829  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3830  					"a": cty.ObjectVal(map[string]cty.Value{
  3831  						"volume_type": cty.StringVal("gp2"),
  3832  						"new_field":   cty.StringVal("new_value"),
  3833  					}),
  3834  				}),
  3835  			}),
  3836  			RequiredReplace: cty.NewPathSet(),
  3837  			Schema:          testSchemaPlus(configschema.NestingMap),
  3838  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3839    ~ resource "test_instance" "example" {
  3840        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3841        ~ disks = {
  3842            - "disk_a" = {
  3843                - mount_point = "/var/diska" -> null
  3844                - size        = "50GB" -> null
  3845              },
  3846          } -> (known after apply)
  3847          id    = "i-02ae66f368e8518a9"
  3848  
  3849          # (1 unchanged block hidden)
  3850      }
  3851  `,
  3852  		},
  3853  		"in-place update - insertion sensitive": {
  3854  			Action: plans.Update,
  3855  			Mode:   addrs.ManagedResourceMode,
  3856  			Before: cty.ObjectVal(map[string]cty.Value{
  3857  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3858  				"ami": cty.StringVal("ami-BEFORE"),
  3859  				"disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  3860  					"mount_point": cty.String,
  3861  					"size":        cty.String,
  3862  				})),
  3863  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3864  					"a": cty.ObjectVal(map[string]cty.Value{
  3865  						"volume_type": cty.StringVal("gp2"),
  3866  						"new_field":   cty.StringVal("new_value"),
  3867  					}),
  3868  				}),
  3869  			}),
  3870  			After: cty.ObjectVal(map[string]cty.Value{
  3871  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3872  				"ami": cty.StringVal("ami-AFTER"),
  3873  				"disks": cty.MapVal(map[string]cty.Value{
  3874  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3875  						"mount_point": cty.StringVal("/var/diska"),
  3876  						"size":        cty.StringVal("50GB"),
  3877  					}),
  3878  				}),
  3879  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3880  					"a": cty.ObjectVal(map[string]cty.Value{
  3881  						"volume_type": cty.StringVal("gp2"),
  3882  						"new_field":   cty.StringVal("new_value"),
  3883  					}),
  3884  				}),
  3885  			}),
  3886  			AfterValMarks: []cty.PathValueMarks{
  3887  				{
  3888  					Path: cty.Path{cty.GetAttrStep{Name: "disks"},
  3889  						cty.IndexStep{Key: cty.StringVal("disk_a")},
  3890  						cty.GetAttrStep{Name: "mount_point"},
  3891  					},
  3892  					Marks: cty.NewValueMarks(marks.Sensitive),
  3893  				},
  3894  			},
  3895  			RequiredReplace: cty.NewPathSet(),
  3896  			Schema:          testSchemaPlus(configschema.NestingMap),
  3897  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3898    ~ resource "test_instance" "example" {
  3899        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3900        ~ disks = {
  3901            + "disk_a" = {
  3902                + mount_point = (sensitive)
  3903                + size        = "50GB"
  3904              },
  3905          }
  3906          id    = "i-02ae66f368e8518a9"
  3907  
  3908          # (1 unchanged block hidden)
  3909      }
  3910  `,
  3911  		},
  3912  	}
  3913  	runTestCases(t, testCases)
  3914  }
  3915  
  3916  func TestResourceChange_actionReason(t *testing.T) {
  3917  	emptySchema := &configschema.Block{}
  3918  	nullVal := cty.NullVal(cty.EmptyObject)
  3919  	emptyVal := cty.EmptyObjectVal
  3920  
  3921  	testCases := map[string]testCase{
  3922  		"delete for no particular reason": {
  3923  			Action:          plans.Delete,
  3924  			ActionReason:    plans.ResourceInstanceChangeNoReason,
  3925  			Mode:            addrs.ManagedResourceMode,
  3926  			Before:          emptyVal,
  3927  			After:           nullVal,
  3928  			Schema:          emptySchema,
  3929  			RequiredReplace: cty.NewPathSet(),
  3930  			ExpectedOutput: `  # test_instance.example will be destroyed
  3931    - resource "test_instance" "example" {}
  3932  `,
  3933  		},
  3934  		"delete because of wrong repetition mode (NoKey)": {
  3935  			Action:          plans.Delete,
  3936  			ActionReason:    plans.ResourceInstanceDeleteBecauseWrongRepetition,
  3937  			Mode:            addrs.ManagedResourceMode,
  3938  			InstanceKey:     addrs.NoKey,
  3939  			Before:          emptyVal,
  3940  			After:           nullVal,
  3941  			Schema:          emptySchema,
  3942  			RequiredReplace: cty.NewPathSet(),
  3943  			ExpectedOutput: `  # test_instance.example will be destroyed
  3944    # (because resource uses count or for_each)
  3945    - resource "test_instance" "example" {}
  3946  `,
  3947  		},
  3948  		"delete because of wrong repetition mode (IntKey)": {
  3949  			Action:          plans.Delete,
  3950  			ActionReason:    plans.ResourceInstanceDeleteBecauseWrongRepetition,
  3951  			Mode:            addrs.ManagedResourceMode,
  3952  			InstanceKey:     addrs.IntKey(1),
  3953  			Before:          emptyVal,
  3954  			After:           nullVal,
  3955  			Schema:          emptySchema,
  3956  			RequiredReplace: cty.NewPathSet(),
  3957  			ExpectedOutput: `  # test_instance.example[1] will be destroyed
  3958    # (because resource does not use count)
  3959    - resource "test_instance" "example" {}
  3960  `,
  3961  		},
  3962  		"delete because of wrong repetition mode (StringKey)": {
  3963  			Action:          plans.Delete,
  3964  			ActionReason:    plans.ResourceInstanceDeleteBecauseWrongRepetition,
  3965  			Mode:            addrs.ManagedResourceMode,
  3966  			InstanceKey:     addrs.StringKey("a"),
  3967  			Before:          emptyVal,
  3968  			After:           nullVal,
  3969  			Schema:          emptySchema,
  3970  			RequiredReplace: cty.NewPathSet(),
  3971  			ExpectedOutput: `  # test_instance.example["a"] will be destroyed
  3972    # (because resource does not use for_each)
  3973    - resource "test_instance" "example" {}
  3974  `,
  3975  		},
  3976  		"delete because no resource configuration": {
  3977  			Action:          plans.Delete,
  3978  			ActionReason:    plans.ResourceInstanceDeleteBecauseNoResourceConfig,
  3979  			ModuleInst:      addrs.RootModuleInstance.Child("foo", addrs.NoKey),
  3980  			Mode:            addrs.ManagedResourceMode,
  3981  			Before:          emptyVal,
  3982  			After:           nullVal,
  3983  			Schema:          emptySchema,
  3984  			RequiredReplace: cty.NewPathSet(),
  3985  			ExpectedOutput: `  # module.foo.test_instance.example will be destroyed
  3986    # (because test_instance.example is not in configuration)
  3987    - resource "test_instance" "example" {}
  3988  `,
  3989  		},
  3990  		"delete because no module": {
  3991  			Action:          plans.Delete,
  3992  			ActionReason:    plans.ResourceInstanceDeleteBecauseNoModule,
  3993  			ModuleInst:      addrs.RootModuleInstance.Child("foo", addrs.IntKey(1)),
  3994  			Mode:            addrs.ManagedResourceMode,
  3995  			Before:          emptyVal,
  3996  			After:           nullVal,
  3997  			Schema:          emptySchema,
  3998  			RequiredReplace: cty.NewPathSet(),
  3999  			ExpectedOutput: `  # module.foo[1].test_instance.example will be destroyed
  4000    # (because module.foo[1] is not in configuration)
  4001    - resource "test_instance" "example" {}
  4002  `,
  4003  		},
  4004  		"delete because out of range for count": {
  4005  			Action:          plans.Delete,
  4006  			ActionReason:    plans.ResourceInstanceDeleteBecauseCountIndex,
  4007  			Mode:            addrs.ManagedResourceMode,
  4008  			InstanceKey:     addrs.IntKey(1),
  4009  			Before:          emptyVal,
  4010  			After:           nullVal,
  4011  			Schema:          emptySchema,
  4012  			RequiredReplace: cty.NewPathSet(),
  4013  			ExpectedOutput: `  # test_instance.example[1] will be destroyed
  4014    # (because index [1] is out of range for count)
  4015    - resource "test_instance" "example" {}
  4016  `,
  4017  		},
  4018  		"delete because out of range for for_each": {
  4019  			Action:          plans.Delete,
  4020  			ActionReason:    plans.ResourceInstanceDeleteBecauseEachKey,
  4021  			Mode:            addrs.ManagedResourceMode,
  4022  			InstanceKey:     addrs.StringKey("boop"),
  4023  			Before:          emptyVal,
  4024  			After:           nullVal,
  4025  			Schema:          emptySchema,
  4026  			RequiredReplace: cty.NewPathSet(),
  4027  			ExpectedOutput: `  # test_instance.example["boop"] will be destroyed
  4028    # (because key ["boop"] is not in for_each map)
  4029    - resource "test_instance" "example" {}
  4030  `,
  4031  		},
  4032  		"replace for no particular reason (delete first)": {
  4033  			Action:          plans.DeleteThenCreate,
  4034  			ActionReason:    plans.ResourceInstanceChangeNoReason,
  4035  			Mode:            addrs.ManagedResourceMode,
  4036  			Before:          emptyVal,
  4037  			After:           nullVal,
  4038  			Schema:          emptySchema,
  4039  			RequiredReplace: cty.NewPathSet(),
  4040  			ExpectedOutput: `  # test_instance.example must be replaced
  4041  -/+ resource "test_instance" "example" {}
  4042  `,
  4043  		},
  4044  		"replace for no particular reason (create first)": {
  4045  			Action:          plans.CreateThenDelete,
  4046  			ActionReason:    plans.ResourceInstanceChangeNoReason,
  4047  			Mode:            addrs.ManagedResourceMode,
  4048  			Before:          emptyVal,
  4049  			After:           nullVal,
  4050  			Schema:          emptySchema,
  4051  			RequiredReplace: cty.NewPathSet(),
  4052  			ExpectedOutput: `  # test_instance.example must be replaced
  4053  +/- resource "test_instance" "example" {}
  4054  `,
  4055  		},
  4056  		"replace by request (delete first)": {
  4057  			Action:          plans.DeleteThenCreate,
  4058  			ActionReason:    plans.ResourceInstanceReplaceByRequest,
  4059  			Mode:            addrs.ManagedResourceMode,
  4060  			Before:          emptyVal,
  4061  			After:           nullVal,
  4062  			Schema:          emptySchema,
  4063  			RequiredReplace: cty.NewPathSet(),
  4064  			ExpectedOutput: `  # test_instance.example will be replaced, as requested
  4065  -/+ resource "test_instance" "example" {}
  4066  `,
  4067  		},
  4068  		"replace by request (create first)": {
  4069  			Action:          plans.CreateThenDelete,
  4070  			ActionReason:    plans.ResourceInstanceReplaceByRequest,
  4071  			Mode:            addrs.ManagedResourceMode,
  4072  			Before:          emptyVal,
  4073  			After:           nullVal,
  4074  			Schema:          emptySchema,
  4075  			RequiredReplace: cty.NewPathSet(),
  4076  			ExpectedOutput: `  # test_instance.example will be replaced, as requested
  4077  +/- resource "test_instance" "example" {}
  4078  `,
  4079  		},
  4080  		"replace because tainted (delete first)": {
  4081  			Action:          plans.DeleteThenCreate,
  4082  			ActionReason:    plans.ResourceInstanceReplaceBecauseTainted,
  4083  			Mode:            addrs.ManagedResourceMode,
  4084  			Before:          emptyVal,
  4085  			After:           nullVal,
  4086  			Schema:          emptySchema,
  4087  			RequiredReplace: cty.NewPathSet(),
  4088  			ExpectedOutput: `  # test_instance.example is tainted, so must be replaced
  4089  -/+ resource "test_instance" "example" {}
  4090  `,
  4091  		},
  4092  		"replace because tainted (create first)": {
  4093  			Action:          plans.CreateThenDelete,
  4094  			ActionReason:    plans.ResourceInstanceReplaceBecauseTainted,
  4095  			Mode:            addrs.ManagedResourceMode,
  4096  			Before:          emptyVal,
  4097  			After:           nullVal,
  4098  			Schema:          emptySchema,
  4099  			RequiredReplace: cty.NewPathSet(),
  4100  			ExpectedOutput: `  # test_instance.example is tainted, so must be replaced
  4101  +/- resource "test_instance" "example" {}
  4102  `,
  4103  		},
  4104  		"replace because cannot update (delete first)": {
  4105  			Action:          plans.DeleteThenCreate,
  4106  			ActionReason:    plans.ResourceInstanceReplaceBecauseCannotUpdate,
  4107  			Mode:            addrs.ManagedResourceMode,
  4108  			Before:          emptyVal,
  4109  			After:           nullVal,
  4110  			Schema:          emptySchema,
  4111  			RequiredReplace: cty.NewPathSet(),
  4112  			// This one has no special message, because the fuller explanation
  4113  			// typically appears inline as a "# forces replacement" comment.
  4114  			// (not shown here)
  4115  			ExpectedOutput: `  # test_instance.example must be replaced
  4116  -/+ resource "test_instance" "example" {}
  4117  `,
  4118  		},
  4119  		"replace because cannot update (create first)": {
  4120  			Action:          plans.CreateThenDelete,
  4121  			ActionReason:    plans.ResourceInstanceReplaceBecauseCannotUpdate,
  4122  			Mode:            addrs.ManagedResourceMode,
  4123  			Before:          emptyVal,
  4124  			After:           nullVal,
  4125  			Schema:          emptySchema,
  4126  			RequiredReplace: cty.NewPathSet(),
  4127  			// This one has no special message, because the fuller explanation
  4128  			// typically appears inline as a "# forces replacement" comment.
  4129  			// (not shown here)
  4130  			ExpectedOutput: `  # test_instance.example must be replaced
  4131  +/- resource "test_instance" "example" {}
  4132  `,
  4133  		},
  4134  	}
  4135  
  4136  	runTestCases(t, testCases)
  4137  }
  4138  
  4139  func TestResourceChange_sensitiveVariable(t *testing.T) {
  4140  	testCases := map[string]testCase{
  4141  		"creation": {
  4142  			Action: plans.Create,
  4143  			Mode:   addrs.ManagedResourceMode,
  4144  			Before: cty.NullVal(cty.EmptyObject),
  4145  			After: cty.ObjectVal(map[string]cty.Value{
  4146  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4147  				"ami": cty.StringVal("ami-123"),
  4148  				"map_key": cty.MapVal(map[string]cty.Value{
  4149  					"breakfast": cty.NumberIntVal(800),
  4150  					"dinner":    cty.NumberIntVal(2000),
  4151  				}),
  4152  				"map_whole": cty.MapVal(map[string]cty.Value{
  4153  					"breakfast": cty.StringVal("pizza"),
  4154  					"dinner":    cty.StringVal("pizza"),
  4155  				}),
  4156  				"list_field": cty.ListVal([]cty.Value{
  4157  					cty.StringVal("hello"),
  4158  					cty.StringVal("friends"),
  4159  					cty.StringVal("!"),
  4160  				}),
  4161  				"nested_block_list": cty.ListVal([]cty.Value{
  4162  					cty.ObjectVal(map[string]cty.Value{
  4163  						"an_attr": cty.StringVal("secretval"),
  4164  						"another": cty.StringVal("not secret"),
  4165  					}),
  4166  				}),
  4167  				"nested_block_set": cty.ListVal([]cty.Value{
  4168  					cty.ObjectVal(map[string]cty.Value{
  4169  						"an_attr": cty.StringVal("secretval"),
  4170  						"another": cty.StringVal("not secret"),
  4171  					}),
  4172  				}),
  4173  			}),
  4174  			AfterValMarks: []cty.PathValueMarks{
  4175  				{
  4176  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  4177  					Marks: cty.NewValueMarks(marks.Sensitive),
  4178  				},
  4179  				{
  4180  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}},
  4181  					Marks: cty.NewValueMarks(marks.Sensitive),
  4182  				},
  4183  				{
  4184  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  4185  					Marks: cty.NewValueMarks(marks.Sensitive),
  4186  				},
  4187  				{
  4188  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  4189  					Marks: cty.NewValueMarks(marks.Sensitive),
  4190  				},
  4191  				{
  4192  					// Nested blocks/sets will mark the whole set/block as sensitive
  4193  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_list"}},
  4194  					Marks: cty.NewValueMarks(marks.Sensitive),
  4195  				},
  4196  				{
  4197  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
  4198  					Marks: cty.NewValueMarks(marks.Sensitive),
  4199  				},
  4200  			},
  4201  			RequiredReplace: cty.NewPathSet(),
  4202  			Schema: &configschema.Block{
  4203  				Attributes: map[string]*configschema.Attribute{
  4204  					"id":         {Type: cty.String, Optional: true, Computed: true},
  4205  					"ami":        {Type: cty.String, Optional: true},
  4206  					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
  4207  					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
  4208  					"list_field": {Type: cty.List(cty.String), Optional: true},
  4209  				},
  4210  				BlockTypes: map[string]*configschema.NestedBlock{
  4211  					"nested_block_list": {
  4212  						Block: configschema.Block{
  4213  							Attributes: map[string]*configschema.Attribute{
  4214  								"an_attr": {Type: cty.String, Optional: true},
  4215  								"another": {Type: cty.String, Optional: true},
  4216  							},
  4217  						},
  4218  						Nesting: configschema.NestingList,
  4219  					},
  4220  					"nested_block_set": {
  4221  						Block: configschema.Block{
  4222  							Attributes: map[string]*configschema.Attribute{
  4223  								"an_attr": {Type: cty.String, Optional: true},
  4224  								"another": {Type: cty.String, Optional: true},
  4225  							},
  4226  						},
  4227  						Nesting: configschema.NestingSet,
  4228  					},
  4229  				},
  4230  			},
  4231  			ExpectedOutput: `  # test_instance.example will be created
  4232    + resource "test_instance" "example" {
  4233        + ami        = (sensitive)
  4234        + id         = "i-02ae66f368e8518a9"
  4235        + list_field = [
  4236            + "hello",
  4237            + (sensitive),
  4238            + "!",
  4239          ]
  4240        + map_key    = {
  4241            + "breakfast" = 800
  4242            + "dinner"    = (sensitive)
  4243          }
  4244        + map_whole  = (sensitive)
  4245  
  4246        + nested_block_list {
  4247            # At least one attribute in this block is (or was) sensitive,
  4248            # so its contents will not be displayed.
  4249          }
  4250  
  4251        + nested_block_set {
  4252            # At least one attribute in this block is (or was) sensitive,
  4253            # so its contents will not be displayed.
  4254          }
  4255      }
  4256  `,
  4257  		},
  4258  		"in-place update - before sensitive": {
  4259  			Action: plans.Update,
  4260  			Mode:   addrs.ManagedResourceMode,
  4261  			Before: cty.ObjectVal(map[string]cty.Value{
  4262  				"id":          cty.StringVal("i-02ae66f368e8518a9"),
  4263  				"ami":         cty.StringVal("ami-BEFORE"),
  4264  				"special":     cty.BoolVal(true),
  4265  				"some_number": cty.NumberIntVal(1),
  4266  				"list_field": cty.ListVal([]cty.Value{
  4267  					cty.StringVal("hello"),
  4268  					cty.StringVal("friends"),
  4269  					cty.StringVal("!"),
  4270  				}),
  4271  				"map_key": cty.MapVal(map[string]cty.Value{
  4272  					"breakfast": cty.NumberIntVal(800),
  4273  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  4274  				}),
  4275  				"map_whole": cty.MapVal(map[string]cty.Value{
  4276  					"breakfast": cty.StringVal("pizza"),
  4277  					"dinner":    cty.StringVal("pizza"),
  4278  				}),
  4279  				"nested_block": cty.ListVal([]cty.Value{
  4280  					cty.ObjectVal(map[string]cty.Value{
  4281  						"an_attr": cty.StringVal("secretval"),
  4282  					}),
  4283  				}),
  4284  				"nested_block_set": cty.ListVal([]cty.Value{
  4285  					cty.ObjectVal(map[string]cty.Value{
  4286  						"an_attr": cty.StringVal("secretval"),
  4287  					}),
  4288  				}),
  4289  			}),
  4290  			After: cty.ObjectVal(map[string]cty.Value{
  4291  				"id":          cty.StringVal("i-02ae66f368e8518a9"),
  4292  				"ami":         cty.StringVal("ami-AFTER"),
  4293  				"special":     cty.BoolVal(false),
  4294  				"some_number": cty.NumberIntVal(2),
  4295  				"list_field": cty.ListVal([]cty.Value{
  4296  					cty.StringVal("hello"),
  4297  					cty.StringVal("friends"),
  4298  					cty.StringVal("."),
  4299  				}),
  4300  				"map_key": cty.MapVal(map[string]cty.Value{
  4301  					"breakfast": cty.NumberIntVal(800),
  4302  					"dinner":    cty.NumberIntVal(1900),
  4303  				}),
  4304  				"map_whole": cty.MapVal(map[string]cty.Value{
  4305  					"breakfast": cty.StringVal("cereal"),
  4306  					"dinner":    cty.StringVal("pizza"),
  4307  				}),
  4308  				"nested_block": cty.ListVal([]cty.Value{
  4309  					cty.ObjectVal(map[string]cty.Value{
  4310  						"an_attr": cty.StringVal("changed"),
  4311  					}),
  4312  				}),
  4313  				"nested_block_set": cty.ListVal([]cty.Value{
  4314  					cty.ObjectVal(map[string]cty.Value{
  4315  						"an_attr": cty.StringVal("changed"),
  4316  					}),
  4317  				}),
  4318  			}),
  4319  			BeforeValMarks: []cty.PathValueMarks{
  4320  				{
  4321  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  4322  					Marks: cty.NewValueMarks(marks.Sensitive),
  4323  				},
  4324  				{
  4325  					Path:  cty.Path{cty.GetAttrStep{Name: "special"}},
  4326  					Marks: cty.NewValueMarks(marks.Sensitive),
  4327  				},
  4328  				{
  4329  					Path:  cty.Path{cty.GetAttrStep{Name: "some_number"}},
  4330  					Marks: cty.NewValueMarks(marks.Sensitive),
  4331  				},
  4332  				{
  4333  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}},
  4334  					Marks: cty.NewValueMarks(marks.Sensitive),
  4335  				},
  4336  				{
  4337  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  4338  					Marks: cty.NewValueMarks(marks.Sensitive),
  4339  				},
  4340  				{
  4341  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  4342  					Marks: cty.NewValueMarks(marks.Sensitive),
  4343  				},
  4344  				{
  4345  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block"}},
  4346  					Marks: cty.NewValueMarks(marks.Sensitive),
  4347  				},
  4348  				{
  4349  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
  4350  					Marks: cty.NewValueMarks(marks.Sensitive),
  4351  				},
  4352  			},
  4353  			RequiredReplace: cty.NewPathSet(),
  4354  			Schema: &configschema.Block{
  4355  				Attributes: map[string]*configschema.Attribute{
  4356  					"id":          {Type: cty.String, Optional: true, Computed: true},
  4357  					"ami":         {Type: cty.String, Optional: true},
  4358  					"list_field":  {Type: cty.List(cty.String), Optional: true},
  4359  					"special":     {Type: cty.Bool, Optional: true},
  4360  					"some_number": {Type: cty.Number, Optional: true},
  4361  					"map_key":     {Type: cty.Map(cty.Number), Optional: true},
  4362  					"map_whole":   {Type: cty.Map(cty.String), Optional: true},
  4363  				},
  4364  				BlockTypes: map[string]*configschema.NestedBlock{
  4365  					"nested_block": {
  4366  						Block: configschema.Block{
  4367  							Attributes: map[string]*configschema.Attribute{
  4368  								"an_attr": {Type: cty.String, Optional: true},
  4369  							},
  4370  						},
  4371  						Nesting: configschema.NestingList,
  4372  					},
  4373  					"nested_block_set": {
  4374  						Block: configschema.Block{
  4375  							Attributes: map[string]*configschema.Attribute{
  4376  								"an_attr": {Type: cty.String, Optional: true},
  4377  							},
  4378  						},
  4379  						Nesting: configschema.NestingSet,
  4380  					},
  4381  				},
  4382  			},
  4383  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4384    ~ resource "test_instance" "example" {
  4385        # Warning: this attribute value will no longer be marked as sensitive
  4386        # after applying this change.
  4387        ~ ami         = (sensitive)
  4388          id          = "i-02ae66f368e8518a9"
  4389        ~ list_field  = [
  4390              # (1 unchanged element hidden)
  4391              "friends",
  4392            - (sensitive),
  4393            + ".",
  4394          ]
  4395        ~ map_key     = {
  4396            # Warning: this attribute value will no longer be marked as sensitive
  4397            # after applying this change.
  4398            ~ "dinner"    = (sensitive)
  4399              # (1 unchanged element hidden)
  4400          }
  4401        # Warning: this attribute value will no longer be marked as sensitive
  4402        # after applying this change.
  4403        ~ map_whole   = (sensitive)
  4404        # Warning: this attribute value will no longer be marked as sensitive
  4405        # after applying this change.
  4406        ~ some_number = (sensitive)
  4407        # Warning: this attribute value will no longer be marked as sensitive
  4408        # after applying this change.
  4409        ~ special     = (sensitive)
  4410  
  4411        # Warning: this block will no longer be marked as sensitive
  4412        # after applying this change.
  4413        ~ nested_block {
  4414            # At least one attribute in this block is (or was) sensitive,
  4415            # so its contents will not be displayed.
  4416          }
  4417  
  4418        # Warning: this block will no longer be marked as sensitive
  4419        # after applying this change.
  4420        ~ nested_block_set {
  4421            # At least one attribute in this block is (or was) sensitive,
  4422            # so its contents will not be displayed.
  4423          }
  4424      }
  4425  `,
  4426  		},
  4427  		"in-place update - after sensitive": {
  4428  			Action: plans.Update,
  4429  			Mode:   addrs.ManagedResourceMode,
  4430  			Before: cty.ObjectVal(map[string]cty.Value{
  4431  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  4432  				"list_field": cty.ListVal([]cty.Value{
  4433  					cty.StringVal("hello"),
  4434  					cty.StringVal("friends"),
  4435  				}),
  4436  				"map_key": cty.MapVal(map[string]cty.Value{
  4437  					"breakfast": cty.NumberIntVal(800),
  4438  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  4439  				}),
  4440  				"map_whole": cty.MapVal(map[string]cty.Value{
  4441  					"breakfast": cty.StringVal("pizza"),
  4442  					"dinner":    cty.StringVal("pizza"),
  4443  				}),
  4444  				"nested_block_single": cty.ObjectVal(map[string]cty.Value{
  4445  					"an_attr": cty.StringVal("original"),
  4446  				}),
  4447  			}),
  4448  			After: cty.ObjectVal(map[string]cty.Value{
  4449  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  4450  				"list_field": cty.ListVal([]cty.Value{
  4451  					cty.StringVal("goodbye"),
  4452  					cty.StringVal("friends"),
  4453  				}),
  4454  				"map_key": cty.MapVal(map[string]cty.Value{
  4455  					"breakfast": cty.NumberIntVal(700),
  4456  					"dinner":    cty.NumberIntVal(2100), // sensitive key
  4457  				}),
  4458  				"map_whole": cty.MapVal(map[string]cty.Value{
  4459  					"breakfast": cty.StringVal("cereal"),
  4460  					"dinner":    cty.StringVal("pizza"),
  4461  				}),
  4462  				"nested_block_single": cty.ObjectVal(map[string]cty.Value{
  4463  					"an_attr": cty.StringVal("changed"),
  4464  				}),
  4465  			}),
  4466  			AfterValMarks: []cty.PathValueMarks{
  4467  				{
  4468  					Path:  cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("address")}},
  4469  					Marks: cty.NewValueMarks(marks.Sensitive),
  4470  				},
  4471  				{
  4472  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
  4473  					Marks: cty.NewValueMarks(marks.Sensitive),
  4474  				},
  4475  				{
  4476  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  4477  					Marks: cty.NewValueMarks(marks.Sensitive),
  4478  				},
  4479  				{
  4480  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  4481  					Marks: cty.NewValueMarks(marks.Sensitive),
  4482  				},
  4483  				{
  4484  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_single"}},
  4485  					Marks: cty.NewValueMarks(marks.Sensitive),
  4486  				},
  4487  			},
  4488  			RequiredReplace: cty.NewPathSet(),
  4489  			Schema: &configschema.Block{
  4490  				Attributes: map[string]*configschema.Attribute{
  4491  					"id":         {Type: cty.String, Optional: true, Computed: true},
  4492  					"list_field": {Type: cty.List(cty.String), Optional: true},
  4493  					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
  4494  					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
  4495  				},
  4496  				BlockTypes: map[string]*configschema.NestedBlock{
  4497  					"nested_block_single": {
  4498  						Block: configschema.Block{
  4499  							Attributes: map[string]*configschema.Attribute{
  4500  								"an_attr": {Type: cty.String, Optional: true},
  4501  							},
  4502  						},
  4503  						Nesting: configschema.NestingSingle,
  4504  					},
  4505  				},
  4506  			},
  4507  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4508    ~ resource "test_instance" "example" {
  4509          id         = "i-02ae66f368e8518a9"
  4510        ~ list_field = [
  4511            - "hello",
  4512            + (sensitive),
  4513              "friends",
  4514          ]
  4515        ~ map_key    = {
  4516            ~ "breakfast" = 800 -> 700
  4517            # Warning: this attribute value will be marked as sensitive and will not
  4518            # display in UI output after applying this change.
  4519            ~ "dinner"    = (sensitive)
  4520          }
  4521        # Warning: this attribute value will be marked as sensitive and will not
  4522        # display in UI output after applying this change.
  4523        ~ map_whole  = (sensitive)
  4524  
  4525        # Warning: this block will be marked as sensitive and will not
  4526        # display in UI output after applying this change.
  4527        ~ nested_block_single {
  4528            # At least one attribute in this block is (or was) sensitive,
  4529            # so its contents will not be displayed.
  4530          }
  4531      }
  4532  `,
  4533  		},
  4534  		"in-place update - both sensitive": {
  4535  			Action: plans.Update,
  4536  			Mode:   addrs.ManagedResourceMode,
  4537  			Before: cty.ObjectVal(map[string]cty.Value{
  4538  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4539  				"ami": cty.StringVal("ami-BEFORE"),
  4540  				"list_field": cty.ListVal([]cty.Value{
  4541  					cty.StringVal("hello"),
  4542  					cty.StringVal("friends"),
  4543  				}),
  4544  				"map_key": cty.MapVal(map[string]cty.Value{
  4545  					"breakfast": cty.NumberIntVal(800),
  4546  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  4547  				}),
  4548  				"map_whole": cty.MapVal(map[string]cty.Value{
  4549  					"breakfast": cty.StringVal("pizza"),
  4550  					"dinner":    cty.StringVal("pizza"),
  4551  				}),
  4552  				"nested_block_map": cty.MapVal(map[string]cty.Value{
  4553  					"foo": cty.ObjectVal(map[string]cty.Value{
  4554  						"an_attr": cty.StringVal("original"),
  4555  					}),
  4556  				}),
  4557  			}),
  4558  			After: cty.ObjectVal(map[string]cty.Value{
  4559  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4560  				"ami": cty.StringVal("ami-AFTER"),
  4561  				"list_field": cty.ListVal([]cty.Value{
  4562  					cty.StringVal("goodbye"),
  4563  					cty.StringVal("friends"),
  4564  				}),
  4565  				"map_key": cty.MapVal(map[string]cty.Value{
  4566  					"breakfast": cty.NumberIntVal(800),
  4567  					"dinner":    cty.NumberIntVal(1800), // sensitive key
  4568  				}),
  4569  				"map_whole": cty.MapVal(map[string]cty.Value{
  4570  					"breakfast": cty.StringVal("cereal"),
  4571  					"dinner":    cty.StringVal("pizza"),
  4572  				}),
  4573  				"nested_block_map": cty.MapVal(map[string]cty.Value{
  4574  					"foo": cty.ObjectVal(map[string]cty.Value{
  4575  						"an_attr": cty.UnknownVal(cty.String),
  4576  					}),
  4577  				}),
  4578  			}),
  4579  			BeforeValMarks: []cty.PathValueMarks{
  4580  				{
  4581  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  4582  					Marks: cty.NewValueMarks(marks.Sensitive),
  4583  				},
  4584  				{
  4585  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
  4586  					Marks: cty.NewValueMarks(marks.Sensitive),
  4587  				},
  4588  				{
  4589  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  4590  					Marks: cty.NewValueMarks(marks.Sensitive),
  4591  				},
  4592  				{
  4593  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  4594  					Marks: cty.NewValueMarks(marks.Sensitive),
  4595  				},
  4596  				{
  4597  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_map"}},
  4598  					Marks: cty.NewValueMarks(marks.Sensitive),
  4599  				},
  4600  			},
  4601  			AfterValMarks: []cty.PathValueMarks{
  4602  				{
  4603  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  4604  					Marks: cty.NewValueMarks(marks.Sensitive),
  4605  				},
  4606  				{
  4607  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
  4608  					Marks: cty.NewValueMarks(marks.Sensitive),
  4609  				},
  4610  				{
  4611  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  4612  					Marks: cty.NewValueMarks(marks.Sensitive),
  4613  				},
  4614  				{
  4615  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  4616  					Marks: cty.NewValueMarks(marks.Sensitive),
  4617  				},
  4618  				{
  4619  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_map"}},
  4620  					Marks: cty.NewValueMarks(marks.Sensitive),
  4621  				},
  4622  			},
  4623  			RequiredReplace: cty.NewPathSet(),
  4624  			Schema: &configschema.Block{
  4625  				Attributes: map[string]*configschema.Attribute{
  4626  					"id":         {Type: cty.String, Optional: true, Computed: true},
  4627  					"ami":        {Type: cty.String, Optional: true},
  4628  					"list_field": {Type: cty.List(cty.String), Optional: true},
  4629  					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
  4630  					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
  4631  				},
  4632  				BlockTypes: map[string]*configschema.NestedBlock{
  4633  					"nested_block_map": {
  4634  						Block: configschema.Block{
  4635  							Attributes: map[string]*configschema.Attribute{
  4636  								"an_attr": {Type: cty.String, Optional: true},
  4637  							},
  4638  						},
  4639  						Nesting: configschema.NestingMap,
  4640  					},
  4641  				},
  4642  			},
  4643  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4644    ~ resource "test_instance" "example" {
  4645        ~ ami        = (sensitive)
  4646          id         = "i-02ae66f368e8518a9"
  4647        ~ list_field = [
  4648            - (sensitive),
  4649            + (sensitive),
  4650              "friends",
  4651          ]
  4652        ~ map_key    = {
  4653            ~ "dinner"    = (sensitive)
  4654              # (1 unchanged element hidden)
  4655          }
  4656        ~ map_whole  = (sensitive)
  4657  
  4658        ~ nested_block_map {
  4659            # At least one attribute in this block is (or was) sensitive,
  4660            # so its contents will not be displayed.
  4661          }
  4662      }
  4663  `,
  4664  		},
  4665  		"in-place update - value unchanged, sensitivity changes": {
  4666  			Action: plans.Update,
  4667  			Mode:   addrs.ManagedResourceMode,
  4668  			Before: cty.ObjectVal(map[string]cty.Value{
  4669  				"id":          cty.StringVal("i-02ae66f368e8518a9"),
  4670  				"ami":         cty.StringVal("ami-BEFORE"),
  4671  				"special":     cty.BoolVal(true),
  4672  				"some_number": cty.NumberIntVal(1),
  4673  				"list_field": cty.ListVal([]cty.Value{
  4674  					cty.StringVal("hello"),
  4675  					cty.StringVal("friends"),
  4676  					cty.StringVal("!"),
  4677  				}),
  4678  				"map_key": cty.MapVal(map[string]cty.Value{
  4679  					"breakfast": cty.NumberIntVal(800),
  4680  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  4681  				}),
  4682  				"map_whole": cty.MapVal(map[string]cty.Value{
  4683  					"breakfast": cty.StringVal("pizza"),
  4684  					"dinner":    cty.StringVal("pizza"),
  4685  				}),
  4686  				"nested_block": cty.ListVal([]cty.Value{
  4687  					cty.ObjectVal(map[string]cty.Value{
  4688  						"an_attr": cty.StringVal("secretval"),
  4689  					}),
  4690  				}),
  4691  				"nested_block_set": cty.ListVal([]cty.Value{
  4692  					cty.ObjectVal(map[string]cty.Value{
  4693  						"an_attr": cty.StringVal("secretval"),
  4694  					}),
  4695  				}),
  4696  			}),
  4697  			After: cty.ObjectVal(map[string]cty.Value{
  4698  				"id":          cty.StringVal("i-02ae66f368e8518a9"),
  4699  				"ami":         cty.StringVal("ami-BEFORE"),
  4700  				"special":     cty.BoolVal(true),
  4701  				"some_number": cty.NumberIntVal(1),
  4702  				"list_field": cty.ListVal([]cty.Value{
  4703  					cty.StringVal("hello"),
  4704  					cty.StringVal("friends"),
  4705  					cty.StringVal("!"),
  4706  				}),
  4707  				"map_key": cty.MapVal(map[string]cty.Value{
  4708  					"breakfast": cty.NumberIntVal(800),
  4709  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  4710  				}),
  4711  				"map_whole": cty.MapVal(map[string]cty.Value{
  4712  					"breakfast": cty.StringVal("pizza"),
  4713  					"dinner":    cty.StringVal("pizza"),
  4714  				}),
  4715  				"nested_block": cty.ListVal([]cty.Value{
  4716  					cty.ObjectVal(map[string]cty.Value{
  4717  						"an_attr": cty.StringVal("secretval"),
  4718  					}),
  4719  				}),
  4720  				"nested_block_set": cty.ListVal([]cty.Value{
  4721  					cty.ObjectVal(map[string]cty.Value{
  4722  						"an_attr": cty.StringVal("secretval"),
  4723  					}),
  4724  				}),
  4725  			}),
  4726  			BeforeValMarks: []cty.PathValueMarks{
  4727  				{
  4728  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  4729  					Marks: cty.NewValueMarks(marks.Sensitive),
  4730  				},
  4731  				{
  4732  					Path:  cty.Path{cty.GetAttrStep{Name: "special"}},
  4733  					Marks: cty.NewValueMarks(marks.Sensitive),
  4734  				},
  4735  				{
  4736  					Path:  cty.Path{cty.GetAttrStep{Name: "some_number"}},
  4737  					Marks: cty.NewValueMarks(marks.Sensitive),
  4738  				},
  4739  				{
  4740  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}},
  4741  					Marks: cty.NewValueMarks(marks.Sensitive),
  4742  				},
  4743  				{
  4744  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  4745  					Marks: cty.NewValueMarks(marks.Sensitive),
  4746  				},
  4747  				{
  4748  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  4749  					Marks: cty.NewValueMarks(marks.Sensitive),
  4750  				},
  4751  				{
  4752  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block"}},
  4753  					Marks: cty.NewValueMarks(marks.Sensitive),
  4754  				},
  4755  				{
  4756  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
  4757  					Marks: cty.NewValueMarks(marks.Sensitive),
  4758  				},
  4759  			},
  4760  			RequiredReplace: cty.NewPathSet(),
  4761  			Schema: &configschema.Block{
  4762  				Attributes: map[string]*configschema.Attribute{
  4763  					"id":          {Type: cty.String, Optional: true, Computed: true},
  4764  					"ami":         {Type: cty.String, Optional: true},
  4765  					"list_field":  {Type: cty.List(cty.String), Optional: true},
  4766  					"special":     {Type: cty.Bool, Optional: true},
  4767  					"some_number": {Type: cty.Number, Optional: true},
  4768  					"map_key":     {Type: cty.Map(cty.Number), Optional: true},
  4769  					"map_whole":   {Type: cty.Map(cty.String), Optional: true},
  4770  				},
  4771  				BlockTypes: map[string]*configschema.NestedBlock{
  4772  					"nested_block": {
  4773  						Block: configschema.Block{
  4774  							Attributes: map[string]*configschema.Attribute{
  4775  								"an_attr": {Type: cty.String, Optional: true},
  4776  							},
  4777  						},
  4778  						Nesting: configschema.NestingList,
  4779  					},
  4780  					"nested_block_set": {
  4781  						Block: configschema.Block{
  4782  							Attributes: map[string]*configschema.Attribute{
  4783  								"an_attr": {Type: cty.String, Optional: true},
  4784  							},
  4785  						},
  4786  						Nesting: configschema.NestingSet,
  4787  					},
  4788  				},
  4789  			},
  4790  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4791    ~ resource "test_instance" "example" {
  4792        # Warning: this attribute value will no longer be marked as sensitive
  4793        # after applying this change. The value is unchanged.
  4794        ~ ami         = (sensitive)
  4795          id          = "i-02ae66f368e8518a9"
  4796        ~ list_field  = [
  4797              # (1 unchanged element hidden)
  4798              "friends",
  4799            - (sensitive),
  4800            + "!",
  4801          ]
  4802        ~ map_key     = {
  4803            # Warning: this attribute value will no longer be marked as sensitive
  4804            # after applying this change. The value is unchanged.
  4805            ~ "dinner"    = (sensitive)
  4806              # (1 unchanged element hidden)
  4807          }
  4808        # Warning: this attribute value will no longer be marked as sensitive
  4809        # after applying this change. The value is unchanged.
  4810        ~ map_whole   = (sensitive)
  4811        # Warning: this attribute value will no longer be marked as sensitive
  4812        # after applying this change. The value is unchanged.
  4813        ~ some_number = (sensitive)
  4814        # Warning: this attribute value will no longer be marked as sensitive
  4815        # after applying this change. The value is unchanged.
  4816        ~ special     = (sensitive)
  4817  
  4818        # Warning: this block will no longer be marked as sensitive
  4819        # after applying this change.
  4820        ~ nested_block {
  4821            # At least one attribute in this block is (or was) sensitive,
  4822            # so its contents will not be displayed.
  4823          }
  4824  
  4825        # Warning: this block will no longer be marked as sensitive
  4826        # after applying this change.
  4827        ~ nested_block_set {
  4828            # At least one attribute in this block is (or was) sensitive,
  4829            # so its contents will not be displayed.
  4830          }
  4831      }
  4832  `,
  4833  		},
  4834  		"deletion": {
  4835  			Action: plans.Delete,
  4836  			Mode:   addrs.ManagedResourceMode,
  4837  			Before: cty.ObjectVal(map[string]cty.Value{
  4838  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4839  				"ami": cty.StringVal("ami-BEFORE"),
  4840  				"list_field": cty.ListVal([]cty.Value{
  4841  					cty.StringVal("hello"),
  4842  					cty.StringVal("friends"),
  4843  				}),
  4844  				"map_key": cty.MapVal(map[string]cty.Value{
  4845  					"breakfast": cty.NumberIntVal(800),
  4846  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  4847  				}),
  4848  				"map_whole": cty.MapVal(map[string]cty.Value{
  4849  					"breakfast": cty.StringVal("pizza"),
  4850  					"dinner":    cty.StringVal("pizza"),
  4851  				}),
  4852  				"nested_block": cty.ListVal([]cty.Value{
  4853  					cty.ObjectVal(map[string]cty.Value{
  4854  						"an_attr": cty.StringVal("secret"),
  4855  						"another": cty.StringVal("not secret"),
  4856  					}),
  4857  				}),
  4858  				"nested_block_set": cty.ListVal([]cty.Value{
  4859  					cty.ObjectVal(map[string]cty.Value{
  4860  						"an_attr": cty.StringVal("secret"),
  4861  						"another": cty.StringVal("not secret"),
  4862  					}),
  4863  				}),
  4864  			}),
  4865  			After: cty.NullVal(cty.EmptyObject),
  4866  			BeforeValMarks: []cty.PathValueMarks{
  4867  				{
  4868  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  4869  					Marks: cty.NewValueMarks(marks.Sensitive),
  4870  				},
  4871  				{
  4872  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}},
  4873  					Marks: cty.NewValueMarks(marks.Sensitive),
  4874  				},
  4875  				{
  4876  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  4877  					Marks: cty.NewValueMarks(marks.Sensitive),
  4878  				},
  4879  				{
  4880  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  4881  					Marks: cty.NewValueMarks(marks.Sensitive),
  4882  				},
  4883  				{
  4884  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block"}},
  4885  					Marks: cty.NewValueMarks(marks.Sensitive),
  4886  				},
  4887  				{
  4888  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
  4889  					Marks: cty.NewValueMarks(marks.Sensitive),
  4890  				},
  4891  			},
  4892  			RequiredReplace: cty.NewPathSet(),
  4893  			Schema: &configschema.Block{
  4894  				Attributes: map[string]*configschema.Attribute{
  4895  					"id":         {Type: cty.String, Optional: true, Computed: true},
  4896  					"ami":        {Type: cty.String, Optional: true},
  4897  					"list_field": {Type: cty.List(cty.String), Optional: true},
  4898  					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
  4899  					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
  4900  				},
  4901  				BlockTypes: map[string]*configschema.NestedBlock{
  4902  					"nested_block_set": {
  4903  						Block: configschema.Block{
  4904  							Attributes: map[string]*configschema.Attribute{
  4905  								"an_attr": {Type: cty.String, Optional: true},
  4906  								"another": {Type: cty.String, Optional: true},
  4907  							},
  4908  						},
  4909  						Nesting: configschema.NestingSet,
  4910  					},
  4911  				},
  4912  			},
  4913  			ExpectedOutput: `  # test_instance.example will be destroyed
  4914    - resource "test_instance" "example" {
  4915        - ami        = (sensitive) -> null
  4916        - id         = "i-02ae66f368e8518a9" -> null
  4917        - list_field = [
  4918            - "hello",
  4919            - (sensitive),
  4920          ] -> null
  4921        - map_key    = {
  4922            - "breakfast" = 800
  4923            - "dinner"    = (sensitive)
  4924          } -> null
  4925        - map_whole  = (sensitive) -> null
  4926  
  4927        - nested_block_set {
  4928            # At least one attribute in this block is (or was) sensitive,
  4929            # so its contents will not be displayed.
  4930          }
  4931      }
  4932  `,
  4933  		},
  4934  		"update with sensitive value forcing replacement": {
  4935  			Action: plans.DeleteThenCreate,
  4936  			Mode:   addrs.ManagedResourceMode,
  4937  			Before: cty.ObjectVal(map[string]cty.Value{
  4938  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4939  				"ami": cty.StringVal("ami-BEFORE"),
  4940  				"nested_block_set": cty.SetVal([]cty.Value{
  4941  					cty.ObjectVal(map[string]cty.Value{
  4942  						"an_attr": cty.StringVal("secret"),
  4943  					}),
  4944  				}),
  4945  			}),
  4946  			After: cty.ObjectVal(map[string]cty.Value{
  4947  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4948  				"ami": cty.StringVal("ami-AFTER"),
  4949  				"nested_block_set": cty.SetVal([]cty.Value{
  4950  					cty.ObjectVal(map[string]cty.Value{
  4951  						"an_attr": cty.StringVal("changed"),
  4952  					}),
  4953  				}),
  4954  			}),
  4955  			BeforeValMarks: []cty.PathValueMarks{
  4956  				{
  4957  					Path:  cty.GetAttrPath("ami"),
  4958  					Marks: cty.NewValueMarks(marks.Sensitive),
  4959  				},
  4960  				{
  4961  					Path:  cty.GetAttrPath("nested_block_set"),
  4962  					Marks: cty.NewValueMarks(marks.Sensitive),
  4963  				},
  4964  			},
  4965  			AfterValMarks: []cty.PathValueMarks{
  4966  				{
  4967  					Path:  cty.GetAttrPath("ami"),
  4968  					Marks: cty.NewValueMarks(marks.Sensitive),
  4969  				},
  4970  				{
  4971  					Path:  cty.GetAttrPath("nested_block_set"),
  4972  					Marks: cty.NewValueMarks(marks.Sensitive),
  4973  				},
  4974  			},
  4975  			Schema: &configschema.Block{
  4976  				Attributes: map[string]*configschema.Attribute{
  4977  					"id":  {Type: cty.String, Optional: true, Computed: true},
  4978  					"ami": {Type: cty.String, Optional: true},
  4979  				},
  4980  				BlockTypes: map[string]*configschema.NestedBlock{
  4981  					"nested_block_set": {
  4982  						Block: configschema.Block{
  4983  							Attributes: map[string]*configschema.Attribute{
  4984  								"an_attr": {Type: cty.String, Required: true},
  4985  							},
  4986  						},
  4987  						Nesting: configschema.NestingSet,
  4988  					},
  4989  				},
  4990  			},
  4991  			RequiredReplace: cty.NewPathSet(
  4992  				cty.GetAttrPath("ami"),
  4993  				cty.GetAttrPath("nested_block_set"),
  4994  			),
  4995  			ExpectedOutput: `  # test_instance.example must be replaced
  4996  -/+ resource "test_instance" "example" {
  4997        ~ ami = (sensitive) # forces replacement
  4998          id  = "i-02ae66f368e8518a9"
  4999  
  5000        ~ nested_block_set { # forces replacement
  5001            # At least one attribute in this block is (or was) sensitive,
  5002            # so its contents will not be displayed.
  5003          }
  5004      }
  5005  `,
  5006  		},
  5007  		"update with sensitive attribute forcing replacement": {
  5008  			Action: plans.DeleteThenCreate,
  5009  			Mode:   addrs.ManagedResourceMode,
  5010  			Before: cty.ObjectVal(map[string]cty.Value{
  5011  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5012  				"ami": cty.StringVal("ami-BEFORE"),
  5013  			}),
  5014  			After: cty.ObjectVal(map[string]cty.Value{
  5015  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  5016  				"ami": cty.StringVal("ami-AFTER"),
  5017  			}),
  5018  			Schema: &configschema.Block{
  5019  				Attributes: map[string]*configschema.Attribute{
  5020  					"id":  {Type: cty.String, Optional: true, Computed: true},
  5021  					"ami": {Type: cty.String, Optional: true, Computed: true, Sensitive: true},
  5022  				},
  5023  			},
  5024  			RequiredReplace: cty.NewPathSet(
  5025  				cty.GetAttrPath("ami"),
  5026  			),
  5027  			ExpectedOutput: `  # test_instance.example must be replaced
  5028  -/+ resource "test_instance" "example" {
  5029        ~ ami = (sensitive value) # forces replacement
  5030          id  = "i-02ae66f368e8518a9"
  5031      }
  5032  `,
  5033  		},
  5034  		"update with sensitive nested type attribute forcing replacement": {
  5035  			Action: plans.DeleteThenCreate,
  5036  			Mode:   addrs.ManagedResourceMode,
  5037  			Before: cty.ObjectVal(map[string]cty.Value{
  5038  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  5039  				"conn_info": cty.ObjectVal(map[string]cty.Value{
  5040  					"user":     cty.StringVal("not-secret"),
  5041  					"password": cty.StringVal("top-secret"),
  5042  				}),
  5043  			}),
  5044  			After: cty.ObjectVal(map[string]cty.Value{
  5045  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  5046  				"conn_info": cty.ObjectVal(map[string]cty.Value{
  5047  					"user":     cty.StringVal("not-secret"),
  5048  					"password": cty.StringVal("new-secret"),
  5049  				}),
  5050  			}),
  5051  			Schema: &configschema.Block{
  5052  				Attributes: map[string]*configschema.Attribute{
  5053  					"id": {Type: cty.String, Optional: true, Computed: true},
  5054  					"conn_info": {
  5055  						NestedType: &configschema.Object{
  5056  							Nesting: configschema.NestingSingle,
  5057  							Attributes: map[string]*configschema.Attribute{
  5058  								"user":     {Type: cty.String, Optional: true},
  5059  								"password": {Type: cty.String, Optional: true, Sensitive: true},
  5060  							},
  5061  						},
  5062  					},
  5063  				},
  5064  			},
  5065  			RequiredReplace: cty.NewPathSet(
  5066  				cty.GetAttrPath("conn_info"),
  5067  				cty.GetAttrPath("password"),
  5068  			),
  5069  			ExpectedOutput: `  # test_instance.example must be replaced
  5070  -/+ resource "test_instance" "example" {
  5071        ~ conn_info = { # forces replacement
  5072            ~ password = (sensitive value)
  5073              # (1 unchanged attribute hidden)
  5074          }
  5075          id        = "i-02ae66f368e8518a9"
  5076      }
  5077  `,
  5078  		},
  5079  	}
  5080  	runTestCases(t, testCases)
  5081  }
  5082  
  5083  func TestResourceChange_moved(t *testing.T) {
  5084  	prevRunAddr := addrs.Resource{
  5085  		Mode: addrs.ManagedResourceMode,
  5086  		Type: "test_instance",
  5087  		Name: "previous",
  5088  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
  5089  
  5090  	testCases := map[string]testCase{
  5091  		"moved and updated": {
  5092  			PrevRunAddr: prevRunAddr,
  5093  			Action:      plans.Update,
  5094  			Mode:        addrs.ManagedResourceMode,
  5095  			Before: cty.ObjectVal(map[string]cty.Value{
  5096  				"id":  cty.StringVal("12345"),
  5097  				"foo": cty.StringVal("hello"),
  5098  				"bar": cty.StringVal("baz"),
  5099  			}),
  5100  			After: cty.ObjectVal(map[string]cty.Value{
  5101  				"id":  cty.StringVal("12345"),
  5102  				"foo": cty.StringVal("hello"),
  5103  				"bar": cty.StringVal("boop"),
  5104  			}),
  5105  			Schema: &configschema.Block{
  5106  				Attributes: map[string]*configschema.Attribute{
  5107  					"id":  {Type: cty.String, Computed: true},
  5108  					"foo": {Type: cty.String, Optional: true},
  5109  					"bar": {Type: cty.String, Optional: true},
  5110  				},
  5111  			},
  5112  			RequiredReplace: cty.NewPathSet(),
  5113  			ExpectedOutput: `  # test_instance.example will be updated in-place
  5114    # (moved from test_instance.previous)
  5115    ~ resource "test_instance" "example" {
  5116        ~ bar = "baz" -> "boop"
  5117          id  = "12345"
  5118          # (1 unchanged attribute hidden)
  5119      }
  5120  `,
  5121  		},
  5122  		"moved without changes": {
  5123  			PrevRunAddr: prevRunAddr,
  5124  			Action:      plans.NoOp,
  5125  			Mode:        addrs.ManagedResourceMode,
  5126  			Before: cty.ObjectVal(map[string]cty.Value{
  5127  				"id":  cty.StringVal("12345"),
  5128  				"foo": cty.StringVal("hello"),
  5129  				"bar": cty.StringVal("baz"),
  5130  			}),
  5131  			After: cty.ObjectVal(map[string]cty.Value{
  5132  				"id":  cty.StringVal("12345"),
  5133  				"foo": cty.StringVal("hello"),
  5134  				"bar": cty.StringVal("baz"),
  5135  			}),
  5136  			Schema: &configschema.Block{
  5137  				Attributes: map[string]*configschema.Attribute{
  5138  					"id":  {Type: cty.String, Computed: true},
  5139  					"foo": {Type: cty.String, Optional: true},
  5140  					"bar": {Type: cty.String, Optional: true},
  5141  				},
  5142  			},
  5143  			RequiredReplace: cty.NewPathSet(),
  5144  			ExpectedOutput: `  # test_instance.previous has moved to test_instance.example
  5145      resource "test_instance" "example" {
  5146          id  = "12345"
  5147          # (2 unchanged attributes hidden)
  5148      }
  5149  `,
  5150  		},
  5151  	}
  5152  
  5153  	runTestCases(t, testCases)
  5154  }
  5155  
  5156  type testCase struct {
  5157  	Action          plans.Action
  5158  	ActionReason    plans.ResourceInstanceChangeActionReason
  5159  	ModuleInst      addrs.ModuleInstance
  5160  	Mode            addrs.ResourceMode
  5161  	InstanceKey     addrs.InstanceKey
  5162  	DeposedKey      states.DeposedKey
  5163  	Before          cty.Value
  5164  	BeforeValMarks  []cty.PathValueMarks
  5165  	AfterValMarks   []cty.PathValueMarks
  5166  	After           cty.Value
  5167  	Schema          *configschema.Block
  5168  	RequiredReplace cty.PathSet
  5169  	ExpectedOutput  string
  5170  	PrevRunAddr     addrs.AbsResourceInstance
  5171  }
  5172  
  5173  func runTestCases(t *testing.T, testCases map[string]testCase) {
  5174  	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
  5175  
  5176  	for name, tc := range testCases {
  5177  		t.Run(name, func(t *testing.T) {
  5178  			ty := tc.Schema.ImpliedType()
  5179  
  5180  			beforeVal := tc.Before
  5181  			switch { // Some fixups to make the test cases a little easier to write
  5182  			case beforeVal.IsNull():
  5183  				beforeVal = cty.NullVal(ty) // allow mistyped nulls
  5184  			case !beforeVal.IsKnown():
  5185  				beforeVal = cty.UnknownVal(ty) // allow mistyped unknowns
  5186  			}
  5187  			before, err := plans.NewDynamicValue(beforeVal, ty)
  5188  			if err != nil {
  5189  				t.Fatal(err)
  5190  			}
  5191  
  5192  			afterVal := tc.After
  5193  			switch { // Some fixups to make the test cases a little easier to write
  5194  			case afterVal.IsNull():
  5195  				afterVal = cty.NullVal(ty) // allow mistyped nulls
  5196  			case !afterVal.IsKnown():
  5197  				afterVal = cty.UnknownVal(ty) // allow mistyped unknowns
  5198  			}
  5199  			after, err := plans.NewDynamicValue(afterVal, ty)
  5200  			if err != nil {
  5201  				t.Fatal(err)
  5202  			}
  5203  
  5204  			addr := addrs.Resource{
  5205  				Mode: tc.Mode,
  5206  				Type: "test_instance",
  5207  				Name: "example",
  5208  			}.Instance(tc.InstanceKey).Absolute(tc.ModuleInst)
  5209  
  5210  			prevRunAddr := tc.PrevRunAddr
  5211  			// If no previous run address is given, reuse the current address
  5212  			// to make initialization easier
  5213  			if prevRunAddr.Resource.Resource.Type == "" {
  5214  				prevRunAddr = addr
  5215  			}
  5216  
  5217  			change := &plans.ResourceInstanceChangeSrc{
  5218  				Addr:        addr,
  5219  				PrevRunAddr: prevRunAddr,
  5220  				DeposedKey:  tc.DeposedKey,
  5221  				ProviderAddr: addrs.AbsProviderConfig{
  5222  					Provider: addrs.NewDefaultProvider("test"),
  5223  					Module:   addrs.RootModule,
  5224  				},
  5225  				ChangeSrc: plans.ChangeSrc{
  5226  					Action:         tc.Action,
  5227  					Before:         before,
  5228  					After:          after,
  5229  					BeforeValMarks: tc.BeforeValMarks,
  5230  					AfterValMarks:  tc.AfterValMarks,
  5231  				},
  5232  				ActionReason:    tc.ActionReason,
  5233  				RequiredReplace: tc.RequiredReplace,
  5234  			}
  5235  
  5236  			output := ResourceChange(change, tc.Schema, color, DiffLanguageProposedChange)
  5237  			if diff := cmp.Diff(output, tc.ExpectedOutput); diff != "" {
  5238  				t.Errorf("wrong output\n%s", diff)
  5239  			}
  5240  		})
  5241  	}
  5242  }
  5243  
  5244  func TestOutputChanges(t *testing.T) {
  5245  	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
  5246  
  5247  	testCases := map[string]struct {
  5248  		changes []*plans.OutputChangeSrc
  5249  		output  string
  5250  	}{
  5251  		"new output value": {
  5252  			[]*plans.OutputChangeSrc{
  5253  				outputChange(
  5254  					"foo",
  5255  					cty.NullVal(cty.DynamicPseudoType),
  5256  					cty.StringVal("bar"),
  5257  					false,
  5258  				),
  5259  			},
  5260  			`
  5261    + foo = "bar"`,
  5262  		},
  5263  		"removed output": {
  5264  			[]*plans.OutputChangeSrc{
  5265  				outputChange(
  5266  					"foo",
  5267  					cty.StringVal("bar"),
  5268  					cty.NullVal(cty.DynamicPseudoType),
  5269  					false,
  5270  				),
  5271  			},
  5272  			`
  5273    - foo = "bar" -> null`,
  5274  		},
  5275  		"single string change": {
  5276  			[]*plans.OutputChangeSrc{
  5277  				outputChange(
  5278  					"foo",
  5279  					cty.StringVal("bar"),
  5280  					cty.StringVal("baz"),
  5281  					false,
  5282  				),
  5283  			},
  5284  			`
  5285    ~ foo = "bar" -> "baz"`,
  5286  		},
  5287  		"element added to list": {
  5288  			[]*plans.OutputChangeSrc{
  5289  				outputChange(
  5290  					"foo",
  5291  					cty.ListVal([]cty.Value{
  5292  						cty.StringVal("alpha"),
  5293  						cty.StringVal("beta"),
  5294  						cty.StringVal("delta"),
  5295  						cty.StringVal("epsilon"),
  5296  					}),
  5297  					cty.ListVal([]cty.Value{
  5298  						cty.StringVal("alpha"),
  5299  						cty.StringVal("beta"),
  5300  						cty.StringVal("gamma"),
  5301  						cty.StringVal("delta"),
  5302  						cty.StringVal("epsilon"),
  5303  					}),
  5304  					false,
  5305  				),
  5306  			},
  5307  			`
  5308    ~ foo = [
  5309          # (1 unchanged element hidden)
  5310          "beta",
  5311        + "gamma",
  5312          "delta",
  5313          # (1 unchanged element hidden)
  5314      ]`,
  5315  		},
  5316  		"multiple outputs changed, one sensitive": {
  5317  			[]*plans.OutputChangeSrc{
  5318  				outputChange(
  5319  					"a",
  5320  					cty.NumberIntVal(1),
  5321  					cty.NumberIntVal(2),
  5322  					false,
  5323  				),
  5324  				outputChange(
  5325  					"b",
  5326  					cty.StringVal("hunter2"),
  5327  					cty.StringVal("correct-horse-battery-staple"),
  5328  					true,
  5329  				),
  5330  				outputChange(
  5331  					"c",
  5332  					cty.BoolVal(false),
  5333  					cty.BoolVal(true),
  5334  					false,
  5335  				),
  5336  			},
  5337  			`
  5338    ~ a = 1 -> 2
  5339    ~ b = (sensitive value)
  5340    ~ c = false -> true`,
  5341  		},
  5342  	}
  5343  
  5344  	for name, tc := range testCases {
  5345  		t.Run(name, func(t *testing.T) {
  5346  			output := OutputChanges(tc.changes, color)
  5347  			if output != tc.output {
  5348  				t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.output)
  5349  			}
  5350  		})
  5351  	}
  5352  }
  5353  
  5354  func outputChange(name string, before, after cty.Value, sensitive bool) *plans.OutputChangeSrc {
  5355  	addr := addrs.AbsOutputValue{
  5356  		OutputValue: addrs.OutputValue{Name: name},
  5357  	}
  5358  
  5359  	change := &plans.OutputChange{
  5360  		Addr: addr, Change: plans.Change{
  5361  			Before: before,
  5362  			After:  after,
  5363  		},
  5364  		Sensitive: sensitive,
  5365  	}
  5366  
  5367  	changeSrc, err := change.Encode()
  5368  	if err != nil {
  5369  		panic(fmt.Sprintf("failed to encode change for %s: %s", addr, err))
  5370  	}
  5371  
  5372  	return changeSrc
  5373  }
  5374  
  5375  // A basic test schema using a configurable NestingMode for one (NestedType) attribute and one block
  5376  func testSchema(nesting configschema.NestingMode) *configschema.Block {
  5377  	return &configschema.Block{
  5378  		Attributes: map[string]*configschema.Attribute{
  5379  			"id":  {Type: cty.String, Optional: true, Computed: true},
  5380  			"ami": {Type: cty.String, Optional: true},
  5381  			"disks": {
  5382  				NestedType: &configschema.Object{
  5383  					Attributes: map[string]*configschema.Attribute{
  5384  						"mount_point": {Type: cty.String, Optional: true},
  5385  						"size":        {Type: cty.String, Optional: true},
  5386  					},
  5387  					Nesting: nesting,
  5388  				},
  5389  			},
  5390  		},
  5391  		BlockTypes: map[string]*configschema.NestedBlock{
  5392  			"root_block_device": {
  5393  				Block: configschema.Block{
  5394  					Attributes: map[string]*configschema.Attribute{
  5395  						"volume_type": {
  5396  							Type:     cty.String,
  5397  							Optional: true,
  5398  							Computed: true,
  5399  						},
  5400  					},
  5401  				},
  5402  				Nesting: nesting,
  5403  			},
  5404  		},
  5405  	}
  5406  }
  5407  
  5408  // similar to testSchema with the addition of a "new_field" block
  5409  func testSchemaPlus(nesting configschema.NestingMode) *configschema.Block {
  5410  	return &configschema.Block{
  5411  		Attributes: map[string]*configschema.Attribute{
  5412  			"id":  {Type: cty.String, Optional: true, Computed: true},
  5413  			"ami": {Type: cty.String, Optional: true},
  5414  			"disks": {
  5415  				NestedType: &configschema.Object{
  5416  					Attributes: map[string]*configschema.Attribute{
  5417  						"mount_point": {Type: cty.String, Optional: true},
  5418  						"size":        {Type: cty.String, Optional: true},
  5419  					},
  5420  					Nesting: nesting,
  5421  				},
  5422  			},
  5423  		},
  5424  		BlockTypes: map[string]*configschema.NestedBlock{
  5425  			"root_block_device": {
  5426  				Block: configschema.Block{
  5427  					Attributes: map[string]*configschema.Attribute{
  5428  						"volume_type": {
  5429  							Type:     cty.String,
  5430  							Optional: true,
  5431  							Computed: true,
  5432  						},
  5433  						"new_field": {
  5434  							Type:     cty.String,
  5435  							Optional: true,
  5436  							Computed: true,
  5437  						},
  5438  					},
  5439  				},
  5440  				Nesting: nesting,
  5441  			},
  5442  		},
  5443  	}
  5444  }