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