kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/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  	"kubeform.dev/terraform-backend-sdk/addrs"
     9  	"kubeform.dev/terraform-backend-sdk/configs/configschema"
    10  	"kubeform.dev/terraform-backend-sdk/lang/marks"
    11  	"kubeform.dev/terraform-backend-sdk/plans"
    12  	"kubeform.dev/terraform-backend-sdk/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  	}
  2689  	runTestCases(t, testCases)
  2690  }
  2691  
  2692  func TestResourceChange_nestedSet(t *testing.T) {
  2693  	testCases := map[string]testCase{
  2694  		"in-place update - creation": {
  2695  			Action: plans.Update,
  2696  			Mode:   addrs.ManagedResourceMode,
  2697  			Before: cty.ObjectVal(map[string]cty.Value{
  2698  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2699  				"ami": cty.StringVal("ami-BEFORE"),
  2700  				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  2701  					"mount_point": cty.String,
  2702  					"size":        cty.String,
  2703  				})),
  2704  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  2705  					"volume_type": cty.String,
  2706  				})),
  2707  			}),
  2708  			After: cty.ObjectVal(map[string]cty.Value{
  2709  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2710  				"ami": cty.StringVal("ami-AFTER"),
  2711  				"disks": cty.SetVal([]cty.Value{
  2712  					cty.ObjectVal(map[string]cty.Value{
  2713  						"mount_point": cty.StringVal("/var/diska"),
  2714  						"size":        cty.NullVal(cty.String),
  2715  					}),
  2716  				}),
  2717  				"root_block_device": cty.SetVal([]cty.Value{
  2718  					cty.ObjectVal(map[string]cty.Value{
  2719  						"volume_type": cty.StringVal("gp2"),
  2720  					}),
  2721  				}),
  2722  			}),
  2723  			RequiredReplace: cty.NewPathSet(),
  2724  			Schema:          testSchema(configschema.NestingSet),
  2725  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2726    ~ resource "test_instance" "example" {
  2727        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2728        ~ disks = [
  2729            + {
  2730              + mount_point = "/var/diska"
  2731            },
  2732          ]
  2733          id    = "i-02ae66f368e8518a9"
  2734  
  2735        + root_block_device {
  2736            + volume_type = "gp2"
  2737          }
  2738      }
  2739  `,
  2740  		},
  2741  		"in-place update - insertion": {
  2742  			Action: plans.Update,
  2743  			Mode:   addrs.ManagedResourceMode,
  2744  			Before: cty.ObjectVal(map[string]cty.Value{
  2745  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2746  				"ami": cty.StringVal("ami-BEFORE"),
  2747  				"disks": cty.SetVal([]cty.Value{
  2748  					cty.ObjectVal(map[string]cty.Value{
  2749  						"mount_point": cty.StringVal("/var/diska"),
  2750  						"size":        cty.NullVal(cty.String),
  2751  					}),
  2752  				}),
  2753  				"root_block_device": cty.SetVal([]cty.Value{
  2754  					cty.ObjectVal(map[string]cty.Value{
  2755  						"volume_type": cty.StringVal("gp2"),
  2756  						"new_field":   cty.NullVal(cty.String),
  2757  					}),
  2758  				}),
  2759  			}),
  2760  			After: cty.ObjectVal(map[string]cty.Value{
  2761  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2762  				"ami": cty.StringVal("ami-AFTER"),
  2763  				"disks": cty.SetVal([]cty.Value{
  2764  					cty.ObjectVal(map[string]cty.Value{
  2765  						"mount_point": cty.StringVal("/var/diska"),
  2766  						"size":        cty.StringVal("50GB"),
  2767  					}),
  2768  				}),
  2769  				"root_block_device": cty.SetVal([]cty.Value{
  2770  					cty.ObjectVal(map[string]cty.Value{
  2771  						"volume_type": cty.StringVal("gp2"),
  2772  						"new_field":   cty.StringVal("new_value"),
  2773  					}),
  2774  				}),
  2775  			}),
  2776  			RequiredReplace: cty.NewPathSet(),
  2777  			Schema:          testSchemaPlus(configschema.NestingSet),
  2778  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2779    ~ resource "test_instance" "example" {
  2780        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2781        ~ disks = [
  2782            + {
  2783              + mount_point = "/var/diska"
  2784              + size        = "50GB"
  2785            },
  2786            - {
  2787              - mount_point = "/var/diska" -> null
  2788            },
  2789          ]
  2790          id    = "i-02ae66f368e8518a9"
  2791  
  2792        + root_block_device {
  2793            + new_field   = "new_value"
  2794            + volume_type = "gp2"
  2795          }
  2796        - root_block_device {
  2797            - volume_type = "gp2" -> null
  2798          }
  2799      }
  2800  `,
  2801  		},
  2802  		"force-new update (whole block)": {
  2803  			Action:       plans.DeleteThenCreate,
  2804  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  2805  			Mode:         addrs.ManagedResourceMode,
  2806  			Before: cty.ObjectVal(map[string]cty.Value{
  2807  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2808  				"ami": cty.StringVal("ami-BEFORE"),
  2809  				"root_block_device": cty.SetVal([]cty.Value{
  2810  					cty.ObjectVal(map[string]cty.Value{
  2811  						"volume_type": cty.StringVal("gp2"),
  2812  					}),
  2813  				}),
  2814  				"disks": cty.SetVal([]cty.Value{
  2815  					cty.ObjectVal(map[string]cty.Value{
  2816  						"mount_point": cty.StringVal("/var/diska"),
  2817  						"size":        cty.StringVal("50GB"),
  2818  					}),
  2819  				}),
  2820  			}),
  2821  			After: cty.ObjectVal(map[string]cty.Value{
  2822  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2823  				"ami": cty.StringVal("ami-AFTER"),
  2824  				"root_block_device": cty.SetVal([]cty.Value{
  2825  					cty.ObjectVal(map[string]cty.Value{
  2826  						"volume_type": cty.StringVal("different"),
  2827  					}),
  2828  				}),
  2829  				"disks": cty.SetVal([]cty.Value{
  2830  					cty.ObjectVal(map[string]cty.Value{
  2831  						"mount_point": cty.StringVal("/var/diskb"),
  2832  						"size":        cty.StringVal("50GB"),
  2833  					}),
  2834  				}),
  2835  			}),
  2836  			RequiredReplace: cty.NewPathSet(
  2837  				cty.Path{cty.GetAttrStep{Name: "root_block_device"}},
  2838  				cty.Path{cty.GetAttrStep{Name: "disks"}},
  2839  			),
  2840  			Schema: testSchema(configschema.NestingSet),
  2841  			ExpectedOutput: `  # test_instance.example must be replaced
  2842  -/+ resource "test_instance" "example" {
  2843        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2844        ~ disks = [
  2845            - { # forces replacement
  2846              - mount_point = "/var/diska" -> null
  2847              - size        = "50GB" -> null
  2848            },
  2849            + { # forces replacement
  2850              + mount_point = "/var/diskb"
  2851              + size        = "50GB"
  2852            },
  2853          ]
  2854          id    = "i-02ae66f368e8518a9"
  2855  
  2856        + root_block_device { # forces replacement
  2857            + volume_type = "different"
  2858          }
  2859        - root_block_device { # forces replacement
  2860            - volume_type = "gp2" -> null
  2861          }
  2862      }
  2863  `,
  2864  		},
  2865  		"in-place update - deletion": {
  2866  			Action: plans.Update,
  2867  			Mode:   addrs.ManagedResourceMode,
  2868  			Before: cty.ObjectVal(map[string]cty.Value{
  2869  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2870  				"ami": cty.StringVal("ami-BEFORE"),
  2871  				"root_block_device": cty.SetVal([]cty.Value{
  2872  					cty.ObjectVal(map[string]cty.Value{
  2873  						"volume_type": cty.StringVal("gp2"),
  2874  						"new_field":   cty.StringVal("new_value"),
  2875  					}),
  2876  				}),
  2877  				"disks": cty.SetVal([]cty.Value{
  2878  					cty.ObjectVal(map[string]cty.Value{
  2879  						"mount_point": cty.StringVal("/var/diska"),
  2880  						"size":        cty.StringVal("50GB"),
  2881  					}),
  2882  				}),
  2883  			}),
  2884  			After: cty.ObjectVal(map[string]cty.Value{
  2885  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2886  				"ami": cty.StringVal("ami-AFTER"),
  2887  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  2888  					"volume_type": cty.String,
  2889  					"new_field":   cty.String,
  2890  				})),
  2891  				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  2892  					"mount_point": cty.String,
  2893  					"size":        cty.String,
  2894  				})),
  2895  			}),
  2896  			RequiredReplace: cty.NewPathSet(),
  2897  			Schema:          testSchemaPlus(configschema.NestingSet),
  2898  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2899    ~ resource "test_instance" "example" {
  2900        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2901        ~ disks = [
  2902            - {
  2903              - mount_point = "/var/diska" -> null
  2904              - size        = "50GB" -> null
  2905            },
  2906          ]
  2907          id    = "i-02ae66f368e8518a9"
  2908  
  2909        - root_block_device {
  2910            - new_field   = "new_value" -> null
  2911            - volume_type = "gp2" -> null
  2912          }
  2913      }
  2914  `,
  2915  		},
  2916  		"in-place update - empty nested sets": {
  2917  			Action: plans.Update,
  2918  			Mode:   addrs.ManagedResourceMode,
  2919  			Before: cty.ObjectVal(map[string]cty.Value{
  2920  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2921  				"ami": cty.StringVal("ami-BEFORE"),
  2922  				"disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
  2923  					"mount_point": cty.String,
  2924  					"size":        cty.String,
  2925  				}))),
  2926  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  2927  					"volume_type": cty.String,
  2928  				})),
  2929  			}),
  2930  			After: cty.ObjectVal(map[string]cty.Value{
  2931  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2932  				"ami": cty.StringVal("ami-AFTER"),
  2933  				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  2934  					"mount_point": cty.String,
  2935  					"size":        cty.String,
  2936  				})),
  2937  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  2938  					"volume_type": cty.String,
  2939  				})),
  2940  			}),
  2941  			RequiredReplace: cty.NewPathSet(),
  2942  			Schema:          testSchema(configschema.NestingSet),
  2943  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2944    ~ resource "test_instance" "example" {
  2945        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2946        + disks = [
  2947          ]
  2948          id    = "i-02ae66f368e8518a9"
  2949      }
  2950  `,
  2951  		},
  2952  		"in-place update - null insertion": {
  2953  			Action: plans.Update,
  2954  			Mode:   addrs.ManagedResourceMode,
  2955  			Before: cty.ObjectVal(map[string]cty.Value{
  2956  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2957  				"ami": cty.StringVal("ami-BEFORE"),
  2958  				"disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
  2959  					"mount_point": cty.String,
  2960  					"size":        cty.String,
  2961  				}))),
  2962  				"root_block_device": cty.SetVal([]cty.Value{
  2963  					cty.ObjectVal(map[string]cty.Value{
  2964  						"volume_type": cty.StringVal("gp2"),
  2965  						"new_field":   cty.NullVal(cty.String),
  2966  					}),
  2967  				}),
  2968  			}),
  2969  			After: cty.ObjectVal(map[string]cty.Value{
  2970  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2971  				"ami": cty.StringVal("ami-AFTER"),
  2972  				"disks": cty.SetVal([]cty.Value{
  2973  					cty.ObjectVal(map[string]cty.Value{
  2974  						"mount_point": cty.StringVal("/var/diska"),
  2975  						"size":        cty.StringVal("50GB"),
  2976  					}),
  2977  				}),
  2978  				"root_block_device": cty.SetVal([]cty.Value{
  2979  					cty.ObjectVal(map[string]cty.Value{
  2980  						"volume_type": cty.StringVal("gp2"),
  2981  						"new_field":   cty.StringVal("new_value"),
  2982  					}),
  2983  				}),
  2984  			}),
  2985  			RequiredReplace: cty.NewPathSet(),
  2986  			Schema:          testSchemaPlus(configschema.NestingSet),
  2987  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2988    ~ resource "test_instance" "example" {
  2989        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  2990        + disks = [
  2991            + {
  2992              + mount_point = "/var/diska"
  2993              + size        = "50GB"
  2994            },
  2995          ]
  2996          id    = "i-02ae66f368e8518a9"
  2997  
  2998        + root_block_device {
  2999            + new_field   = "new_value"
  3000            + volume_type = "gp2"
  3001          }
  3002        - root_block_device {
  3003            - volume_type = "gp2" -> null
  3004          }
  3005      }
  3006  `,
  3007  		},
  3008  		"in-place update - unknown": {
  3009  			Action: plans.Update,
  3010  			Mode:   addrs.ManagedResourceMode,
  3011  			Before: cty.ObjectVal(map[string]cty.Value{
  3012  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3013  				"ami": cty.StringVal("ami-BEFORE"),
  3014  				"disks": cty.SetVal([]cty.Value{
  3015  					cty.ObjectVal(map[string]cty.Value{
  3016  						"mount_point": cty.StringVal("/var/diska"),
  3017  						"size":        cty.StringVal("50GB"),
  3018  					}),
  3019  				}),
  3020  				"root_block_device": cty.SetVal([]cty.Value{
  3021  					cty.ObjectVal(map[string]cty.Value{
  3022  						"volume_type": cty.StringVal("gp2"),
  3023  						"new_field":   cty.StringVal("new_value"),
  3024  					}),
  3025  				}),
  3026  			}),
  3027  			After: cty.ObjectVal(map[string]cty.Value{
  3028  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3029  				"ami": cty.StringVal("ami-AFTER"),
  3030  				"disks": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
  3031  					"mount_point": cty.String,
  3032  					"size":        cty.String,
  3033  				}))),
  3034  				"root_block_device": cty.SetVal([]cty.Value{
  3035  					cty.ObjectVal(map[string]cty.Value{
  3036  						"volume_type": cty.StringVal("gp2"),
  3037  						"new_field":   cty.StringVal("new_value"),
  3038  					}),
  3039  				}),
  3040  			}),
  3041  			RequiredReplace: cty.NewPathSet(),
  3042  			Schema:          testSchemaPlus(configschema.NestingSet),
  3043  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3044    ~ resource "test_instance" "example" {
  3045        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3046        ~ disks = [
  3047            - {
  3048              - mount_point = "/var/diska" -> null
  3049              - size        = "50GB" -> null
  3050            },
  3051          ] -> (known after apply)
  3052          id    = "i-02ae66f368e8518a9"
  3053  
  3054          # (1 unchanged block hidden)
  3055      }
  3056  `,
  3057  		},
  3058  	}
  3059  	runTestCases(t, testCases)
  3060  }
  3061  
  3062  func TestResourceChange_nestedMap(t *testing.T) {
  3063  	testCases := map[string]testCase{
  3064  		"creation from null": {
  3065  			Action: plans.Update,
  3066  			Mode:   addrs.ManagedResourceMode,
  3067  			Before: cty.ObjectVal(map[string]cty.Value{
  3068  				"id":  cty.NullVal(cty.String),
  3069  				"ami": cty.NullVal(cty.String),
  3070  				"disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
  3071  					"mount_point": cty.String,
  3072  					"size":        cty.String,
  3073  				}))),
  3074  				"root_block_device": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
  3075  					"volume_type": cty.String,
  3076  				}))),
  3077  			}),
  3078  			After: cty.ObjectVal(map[string]cty.Value{
  3079  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3080  				"ami": cty.StringVal("ami-AFTER"),
  3081  				"disks": cty.MapVal(map[string]cty.Value{
  3082  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3083  						"mount_point": cty.StringVal("/var/diska"),
  3084  						"size":        cty.NullVal(cty.String),
  3085  					}),
  3086  				}),
  3087  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3088  					"a": cty.ObjectVal(map[string]cty.Value{
  3089  						"volume_type": cty.StringVal("gp2"),
  3090  					}),
  3091  				}),
  3092  			}),
  3093  			RequiredReplace: cty.NewPathSet(),
  3094  			Schema:          testSchema(configschema.NestingMap),
  3095  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3096    ~ resource "test_instance" "example" {
  3097        + ami   = "ami-AFTER"
  3098        + disks = {
  3099            + "disk_a" = {
  3100              + mount_point = "/var/diska"
  3101            },
  3102          }
  3103        + id    = "i-02ae66f368e8518a9"
  3104  
  3105        + root_block_device "a" {
  3106            + volume_type = "gp2"
  3107          }
  3108      }
  3109  `,
  3110  		},
  3111  		"in-place update - creation": {
  3112  			Action: plans.Update,
  3113  			Mode:   addrs.ManagedResourceMode,
  3114  			Before: cty.ObjectVal(map[string]cty.Value{
  3115  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3116  				"ami": cty.StringVal("ami-BEFORE"),
  3117  				"disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  3118  					"mount_point": cty.String,
  3119  					"size":        cty.String,
  3120  				})),
  3121  				"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  3122  					"volume_type": cty.String,
  3123  				})),
  3124  			}),
  3125  			After: cty.ObjectVal(map[string]cty.Value{
  3126  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3127  				"ami": cty.StringVal("ami-AFTER"),
  3128  				"disks": cty.MapVal(map[string]cty.Value{
  3129  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3130  						"mount_point": cty.StringVal("/var/diska"),
  3131  						"size":        cty.NullVal(cty.String),
  3132  					}),
  3133  				}),
  3134  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3135  					"a": cty.ObjectVal(map[string]cty.Value{
  3136  						"volume_type": cty.StringVal("gp2"),
  3137  					}),
  3138  				}),
  3139  			}),
  3140  			RequiredReplace: cty.NewPathSet(),
  3141  			Schema:          testSchema(configschema.NestingMap),
  3142  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3143    ~ resource "test_instance" "example" {
  3144        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3145        ~ disks = {
  3146            + "disk_a" = {
  3147              + mount_point = "/var/diska"
  3148            },
  3149          }
  3150          id    = "i-02ae66f368e8518a9"
  3151  
  3152        + root_block_device "a" {
  3153            + volume_type = "gp2"
  3154          }
  3155      }
  3156  `,
  3157  		},
  3158  		"in-place update - change attr": {
  3159  			Action: plans.Update,
  3160  			Mode:   addrs.ManagedResourceMode,
  3161  			Before: cty.ObjectVal(map[string]cty.Value{
  3162  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3163  				"ami": cty.StringVal("ami-BEFORE"),
  3164  				"disks": cty.MapVal(map[string]cty.Value{
  3165  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3166  						"mount_point": cty.StringVal("/var/diska"),
  3167  						"size":        cty.NullVal(cty.String),
  3168  					}),
  3169  				}),
  3170  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3171  					"a": cty.ObjectVal(map[string]cty.Value{
  3172  						"volume_type": cty.StringVal("gp2"),
  3173  						"new_field":   cty.NullVal(cty.String),
  3174  					}),
  3175  				}),
  3176  			}),
  3177  			After: cty.ObjectVal(map[string]cty.Value{
  3178  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3179  				"ami": cty.StringVal("ami-AFTER"),
  3180  				"disks": cty.MapVal(map[string]cty.Value{
  3181  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3182  						"mount_point": cty.StringVal("/var/diska"),
  3183  						"size":        cty.StringVal("50GB"),
  3184  					}),
  3185  				}),
  3186  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3187  					"a": cty.ObjectVal(map[string]cty.Value{
  3188  						"volume_type": cty.StringVal("gp2"),
  3189  						"new_field":   cty.StringVal("new_value"),
  3190  					}),
  3191  				}),
  3192  			}),
  3193  			RequiredReplace: cty.NewPathSet(),
  3194  			Schema:          testSchemaPlus(configschema.NestingMap),
  3195  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3196    ~ resource "test_instance" "example" {
  3197        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3198        ~ disks = {
  3199            ~ "disk_a" = {
  3200              + size        = "50GB"
  3201                # (1 unchanged attribute hidden)
  3202            },
  3203          }
  3204          id    = "i-02ae66f368e8518a9"
  3205  
  3206        ~ root_block_device "a" {
  3207            + new_field   = "new_value"
  3208              # (1 unchanged attribute hidden)
  3209          }
  3210      }
  3211  `,
  3212  		},
  3213  		"in-place update - insertion": {
  3214  			Action: plans.Update,
  3215  			Mode:   addrs.ManagedResourceMode,
  3216  			Before: cty.ObjectVal(map[string]cty.Value{
  3217  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3218  				"ami": cty.StringVal("ami-BEFORE"),
  3219  				"disks": cty.MapVal(map[string]cty.Value{
  3220  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3221  						"mount_point": cty.StringVal("/var/diska"),
  3222  						"size":        cty.StringVal("50GB"),
  3223  					}),
  3224  				}),
  3225  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3226  					"a": cty.ObjectVal(map[string]cty.Value{
  3227  						"volume_type": cty.StringVal("gp2"),
  3228  						"new_field":   cty.NullVal(cty.String),
  3229  					}),
  3230  				}),
  3231  			}),
  3232  			After: cty.ObjectVal(map[string]cty.Value{
  3233  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3234  				"ami": cty.StringVal("ami-AFTER"),
  3235  				"disks": cty.MapVal(map[string]cty.Value{
  3236  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3237  						"mount_point": cty.StringVal("/var/diska"),
  3238  						"size":        cty.StringVal("50GB"),
  3239  					}),
  3240  					"disk_2": cty.ObjectVal(map[string]cty.Value{
  3241  						"mount_point": cty.StringVal("/var/disk2"),
  3242  						"size":        cty.StringVal("50GB"),
  3243  					}),
  3244  				}),
  3245  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3246  					"a": cty.ObjectVal(map[string]cty.Value{
  3247  						"volume_type": cty.StringVal("gp2"),
  3248  						"new_field":   cty.NullVal(cty.String),
  3249  					}),
  3250  					"b": cty.ObjectVal(map[string]cty.Value{
  3251  						"volume_type": cty.StringVal("gp2"),
  3252  						"new_field":   cty.StringVal("new_value"),
  3253  					}),
  3254  				}),
  3255  			}),
  3256  			RequiredReplace: cty.NewPathSet(),
  3257  			Schema:          testSchemaPlus(configschema.NestingMap),
  3258  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3259    ~ resource "test_instance" "example" {
  3260        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3261        ~ disks = {
  3262            + "disk_2" = {
  3263              + mount_point = "/var/disk2"
  3264              + size        = "50GB"
  3265            },
  3266            # (1 unchanged element hidden)
  3267          }
  3268          id    = "i-02ae66f368e8518a9"
  3269  
  3270        + root_block_device "b" {
  3271            + new_field   = "new_value"
  3272            + volume_type = "gp2"
  3273          }
  3274          # (1 unchanged block hidden)
  3275      }
  3276  `,
  3277  		},
  3278  		"force-new update (whole block)": {
  3279  			Action:       plans.DeleteThenCreate,
  3280  			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
  3281  			Mode:         addrs.ManagedResourceMode,
  3282  			Before: cty.ObjectVal(map[string]cty.Value{
  3283  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3284  				"ami": cty.StringVal("ami-BEFORE"),
  3285  				"disks": cty.MapVal(map[string]cty.Value{
  3286  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3287  						"mount_point": cty.StringVal("/var/diska"),
  3288  						"size":        cty.StringVal("50GB"),
  3289  					}),
  3290  				}),
  3291  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3292  					"a": cty.ObjectVal(map[string]cty.Value{
  3293  						"volume_type": cty.StringVal("gp2"),
  3294  					}),
  3295  					"b": cty.ObjectVal(map[string]cty.Value{
  3296  						"volume_type": cty.StringVal("standard"),
  3297  					}),
  3298  				}),
  3299  			}),
  3300  			After: cty.ObjectVal(map[string]cty.Value{
  3301  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3302  				"ami": cty.StringVal("ami-AFTER"),
  3303  				"disks": cty.MapVal(map[string]cty.Value{
  3304  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3305  						"mount_point": cty.StringVal("/var/diska"),
  3306  						"size":        cty.StringVal("100GB"),
  3307  					}),
  3308  				}),
  3309  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3310  					"a": cty.ObjectVal(map[string]cty.Value{
  3311  						"volume_type": cty.StringVal("different"),
  3312  					}),
  3313  					"b": cty.ObjectVal(map[string]cty.Value{
  3314  						"volume_type": cty.StringVal("standard"),
  3315  					}),
  3316  				}),
  3317  			}),
  3318  			RequiredReplace: cty.NewPathSet(cty.Path{
  3319  				cty.GetAttrStep{Name: "root_block_device"},
  3320  				cty.IndexStep{Key: cty.StringVal("a")},
  3321  			},
  3322  				cty.Path{cty.GetAttrStep{Name: "disks"}},
  3323  			),
  3324  			Schema: testSchema(configschema.NestingMap),
  3325  			ExpectedOutput: `  # test_instance.example must be replaced
  3326  -/+ resource "test_instance" "example" {
  3327        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3328        ~ disks = {
  3329            ~ "disk_a" = { # forces replacement
  3330              ~ size        = "50GB" -> "100GB"
  3331                # (1 unchanged attribute hidden)
  3332            },
  3333          }
  3334          id    = "i-02ae66f368e8518a9"
  3335  
  3336        ~ root_block_device "a" { # forces replacement
  3337            ~ volume_type = "gp2" -> "different"
  3338          }
  3339          # (1 unchanged block hidden)
  3340      }
  3341  `,
  3342  		},
  3343  		"in-place update - deletion": {
  3344  			Action: plans.Update,
  3345  			Mode:   addrs.ManagedResourceMode,
  3346  			Before: cty.ObjectVal(map[string]cty.Value{
  3347  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3348  				"ami": cty.StringVal("ami-BEFORE"),
  3349  				"disks": cty.MapVal(map[string]cty.Value{
  3350  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3351  						"mount_point": cty.StringVal("/var/diska"),
  3352  						"size":        cty.StringVal("50GB"),
  3353  					}),
  3354  				}),
  3355  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3356  					"a": cty.ObjectVal(map[string]cty.Value{
  3357  						"volume_type": cty.StringVal("gp2"),
  3358  						"new_field":   cty.StringVal("new_value"),
  3359  					}),
  3360  				}),
  3361  			}),
  3362  			After: cty.ObjectVal(map[string]cty.Value{
  3363  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3364  				"ami": cty.StringVal("ami-AFTER"),
  3365  				"disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  3366  					"mount_point": cty.String,
  3367  					"size":        cty.String,
  3368  				})),
  3369  				"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  3370  					"volume_type": cty.String,
  3371  					"new_field":   cty.String,
  3372  				})),
  3373  			}),
  3374  			RequiredReplace: cty.NewPathSet(),
  3375  			Schema:          testSchemaPlus(configschema.NestingMap),
  3376  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3377    ~ resource "test_instance" "example" {
  3378        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3379        ~ disks = {
  3380            - "disk_a" = {
  3381              - mount_point = "/var/diska" -> null
  3382              - size        = "50GB" -> null
  3383            },
  3384          }
  3385          id    = "i-02ae66f368e8518a9"
  3386  
  3387        - root_block_device "a" {
  3388            - new_field   = "new_value" -> null
  3389            - volume_type = "gp2" -> null
  3390          }
  3391      }
  3392  `,
  3393  		},
  3394  		"in-place update - unknown": {
  3395  			Action: plans.Update,
  3396  			Mode:   addrs.ManagedResourceMode,
  3397  			Before: cty.ObjectVal(map[string]cty.Value{
  3398  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3399  				"ami": cty.StringVal("ami-BEFORE"),
  3400  				"disks": cty.MapVal(map[string]cty.Value{
  3401  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3402  						"mount_point": cty.StringVal("/var/diska"),
  3403  						"size":        cty.StringVal("50GB"),
  3404  					}),
  3405  				}),
  3406  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3407  					"a": cty.ObjectVal(map[string]cty.Value{
  3408  						"volume_type": cty.StringVal("gp2"),
  3409  						"new_field":   cty.StringVal("new_value"),
  3410  					}),
  3411  				}),
  3412  			}),
  3413  			After: cty.ObjectVal(map[string]cty.Value{
  3414  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3415  				"ami": cty.StringVal("ami-AFTER"),
  3416  				"disks": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{
  3417  					"mount_point": cty.String,
  3418  					"size":        cty.String,
  3419  				}))),
  3420  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3421  					"a": cty.ObjectVal(map[string]cty.Value{
  3422  						"volume_type": cty.StringVal("gp2"),
  3423  						"new_field":   cty.StringVal("new_value"),
  3424  					}),
  3425  				}),
  3426  			}),
  3427  			RequiredReplace: cty.NewPathSet(),
  3428  			Schema:          testSchemaPlus(configschema.NestingMap),
  3429  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3430    ~ resource "test_instance" "example" {
  3431        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3432        ~ disks = {
  3433            - "disk_a" = {
  3434              - mount_point = "/var/diska" -> null
  3435              - size        = "50GB" -> null
  3436            },
  3437          } -> (known after apply)
  3438          id    = "i-02ae66f368e8518a9"
  3439  
  3440          # (1 unchanged block hidden)
  3441      }
  3442  `,
  3443  		},
  3444  		"in-place update - insertion sensitive": {
  3445  			Action: plans.Update,
  3446  			Mode:   addrs.ManagedResourceMode,
  3447  			Before: cty.ObjectVal(map[string]cty.Value{
  3448  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3449  				"ami": cty.StringVal("ami-BEFORE"),
  3450  				"disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  3451  					"mount_point": cty.String,
  3452  					"size":        cty.String,
  3453  				})),
  3454  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3455  					"a": cty.ObjectVal(map[string]cty.Value{
  3456  						"volume_type": cty.StringVal("gp2"),
  3457  						"new_field":   cty.StringVal("new_value"),
  3458  					}),
  3459  				}),
  3460  			}),
  3461  			After: cty.ObjectVal(map[string]cty.Value{
  3462  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3463  				"ami": cty.StringVal("ami-AFTER"),
  3464  				"disks": cty.MapVal(map[string]cty.Value{
  3465  					"disk_a": cty.ObjectVal(map[string]cty.Value{
  3466  						"mount_point": cty.StringVal("/var/diska"),
  3467  						"size":        cty.StringVal("50GB"),
  3468  					}),
  3469  				}),
  3470  				"root_block_device": cty.MapVal(map[string]cty.Value{
  3471  					"a": cty.ObjectVal(map[string]cty.Value{
  3472  						"volume_type": cty.StringVal("gp2"),
  3473  						"new_field":   cty.StringVal("new_value"),
  3474  					}),
  3475  				}),
  3476  			}),
  3477  			AfterValMarks: []cty.PathValueMarks{
  3478  				{
  3479  					Path: cty.Path{cty.GetAttrStep{Name: "disks"},
  3480  						cty.IndexStep{Key: cty.StringVal("disk_a")},
  3481  						cty.GetAttrStep{Name: "mount_point"},
  3482  					},
  3483  					Marks: cty.NewValueMarks(marks.Sensitive),
  3484  				},
  3485  			},
  3486  			RequiredReplace: cty.NewPathSet(),
  3487  			Schema:          testSchemaPlus(configschema.NestingMap),
  3488  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3489    ~ resource "test_instance" "example" {
  3490        ~ ami   = "ami-BEFORE" -> "ami-AFTER"
  3491        ~ disks = {
  3492            + "disk_a" = {
  3493              + mount_point = (sensitive)
  3494              + size        = "50GB"
  3495            },
  3496          }
  3497          id    = "i-02ae66f368e8518a9"
  3498  
  3499          # (1 unchanged block hidden)
  3500      }
  3501  `,
  3502  		},
  3503  	}
  3504  	runTestCases(t, testCases)
  3505  }
  3506  
  3507  func TestResourceChange_sensitiveVariable(t *testing.T) {
  3508  	testCases := map[string]testCase{
  3509  		"creation": {
  3510  			Action: plans.Create,
  3511  			Mode:   addrs.ManagedResourceMode,
  3512  			Before: cty.NullVal(cty.EmptyObject),
  3513  			After: cty.ObjectVal(map[string]cty.Value{
  3514  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3515  				"ami": cty.StringVal("ami-123"),
  3516  				"map_key": cty.MapVal(map[string]cty.Value{
  3517  					"breakfast": cty.NumberIntVal(800),
  3518  					"dinner":    cty.NumberIntVal(2000),
  3519  				}),
  3520  				"map_whole": cty.MapVal(map[string]cty.Value{
  3521  					"breakfast": cty.StringVal("pizza"),
  3522  					"dinner":    cty.StringVal("pizza"),
  3523  				}),
  3524  				"list_field": cty.ListVal([]cty.Value{
  3525  					cty.StringVal("hello"),
  3526  					cty.StringVal("friends"),
  3527  					cty.StringVal("!"),
  3528  				}),
  3529  				"nested_block_list": cty.ListVal([]cty.Value{
  3530  					cty.ObjectVal(map[string]cty.Value{
  3531  						"an_attr": cty.StringVal("secretval"),
  3532  						"another": cty.StringVal("not secret"),
  3533  					}),
  3534  				}),
  3535  				"nested_block_set": cty.ListVal([]cty.Value{
  3536  					cty.ObjectVal(map[string]cty.Value{
  3537  						"an_attr": cty.StringVal("secretval"),
  3538  						"another": cty.StringVal("not secret"),
  3539  					}),
  3540  				}),
  3541  			}),
  3542  			AfterValMarks: []cty.PathValueMarks{
  3543  				{
  3544  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  3545  					Marks: cty.NewValueMarks(marks.Sensitive),
  3546  				},
  3547  				{
  3548  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}},
  3549  					Marks: cty.NewValueMarks(marks.Sensitive),
  3550  				},
  3551  				{
  3552  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  3553  					Marks: cty.NewValueMarks(marks.Sensitive),
  3554  				},
  3555  				{
  3556  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  3557  					Marks: cty.NewValueMarks(marks.Sensitive),
  3558  				},
  3559  				{
  3560  					// Nested blocks/sets will mark the whole set/block as sensitive
  3561  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_list"}},
  3562  					Marks: cty.NewValueMarks(marks.Sensitive),
  3563  				},
  3564  				{
  3565  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
  3566  					Marks: cty.NewValueMarks(marks.Sensitive),
  3567  				},
  3568  			},
  3569  			RequiredReplace: cty.NewPathSet(),
  3570  			Schema: &configschema.Block{
  3571  				Attributes: map[string]*configschema.Attribute{
  3572  					"id":         {Type: cty.String, Optional: true, Computed: true},
  3573  					"ami":        {Type: cty.String, Optional: true},
  3574  					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
  3575  					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
  3576  					"list_field": {Type: cty.List(cty.String), Optional: true},
  3577  				},
  3578  				BlockTypes: map[string]*configschema.NestedBlock{
  3579  					"nested_block_list": {
  3580  						Block: configschema.Block{
  3581  							Attributes: map[string]*configschema.Attribute{
  3582  								"an_attr": {Type: cty.String, Optional: true},
  3583  								"another": {Type: cty.String, Optional: true},
  3584  							},
  3585  						},
  3586  						Nesting: configschema.NestingList,
  3587  					},
  3588  					"nested_block_set": {
  3589  						Block: configschema.Block{
  3590  							Attributes: map[string]*configschema.Attribute{
  3591  								"an_attr": {Type: cty.String, Optional: true},
  3592  								"another": {Type: cty.String, Optional: true},
  3593  							},
  3594  						},
  3595  						Nesting: configschema.NestingSet,
  3596  					},
  3597  				},
  3598  			},
  3599  			ExpectedOutput: `  # test_instance.example will be created
  3600    + resource "test_instance" "example" {
  3601        + ami        = (sensitive)
  3602        + id         = "i-02ae66f368e8518a9"
  3603        + list_field = [
  3604            + "hello",
  3605            + (sensitive),
  3606            + "!",
  3607          ]
  3608        + map_key    = {
  3609            + "breakfast" = 800
  3610            + "dinner"    = (sensitive)
  3611          }
  3612        + map_whole  = (sensitive)
  3613  
  3614        + nested_block_list {
  3615            # At least one attribute in this block is (or was) sensitive,
  3616            # so its contents will not be displayed.
  3617          }
  3618  
  3619        + nested_block_set {
  3620            # At least one attribute in this block is (or was) sensitive,
  3621            # so its contents will not be displayed.
  3622          }
  3623      }
  3624  `,
  3625  		},
  3626  		"in-place update - before sensitive": {
  3627  			Action: plans.Update,
  3628  			Mode:   addrs.ManagedResourceMode,
  3629  			Before: cty.ObjectVal(map[string]cty.Value{
  3630  				"id":          cty.StringVal("i-02ae66f368e8518a9"),
  3631  				"ami":         cty.StringVal("ami-BEFORE"),
  3632  				"special":     cty.BoolVal(true),
  3633  				"some_number": cty.NumberIntVal(1),
  3634  				"list_field": cty.ListVal([]cty.Value{
  3635  					cty.StringVal("hello"),
  3636  					cty.StringVal("friends"),
  3637  					cty.StringVal("!"),
  3638  				}),
  3639  				"map_key": cty.MapVal(map[string]cty.Value{
  3640  					"breakfast": cty.NumberIntVal(800),
  3641  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  3642  				}),
  3643  				"map_whole": cty.MapVal(map[string]cty.Value{
  3644  					"breakfast": cty.StringVal("pizza"),
  3645  					"dinner":    cty.StringVal("pizza"),
  3646  				}),
  3647  				"nested_block": cty.ListVal([]cty.Value{
  3648  					cty.ObjectVal(map[string]cty.Value{
  3649  						"an_attr": cty.StringVal("secretval"),
  3650  					}),
  3651  				}),
  3652  				"nested_block_set": cty.ListVal([]cty.Value{
  3653  					cty.ObjectVal(map[string]cty.Value{
  3654  						"an_attr": cty.StringVal("secretval"),
  3655  					}),
  3656  				}),
  3657  			}),
  3658  			After: cty.ObjectVal(map[string]cty.Value{
  3659  				"id":          cty.StringVal("i-02ae66f368e8518a9"),
  3660  				"ami":         cty.StringVal("ami-AFTER"),
  3661  				"special":     cty.BoolVal(false),
  3662  				"some_number": cty.NumberIntVal(2),
  3663  				"list_field": cty.ListVal([]cty.Value{
  3664  					cty.StringVal("hello"),
  3665  					cty.StringVal("friends"),
  3666  					cty.StringVal("."),
  3667  				}),
  3668  				"map_key": cty.MapVal(map[string]cty.Value{
  3669  					"breakfast": cty.NumberIntVal(800),
  3670  					"dinner":    cty.NumberIntVal(1900),
  3671  				}),
  3672  				"map_whole": cty.MapVal(map[string]cty.Value{
  3673  					"breakfast": cty.StringVal("cereal"),
  3674  					"dinner":    cty.StringVal("pizza"),
  3675  				}),
  3676  				"nested_block": cty.ListVal([]cty.Value{
  3677  					cty.ObjectVal(map[string]cty.Value{
  3678  						"an_attr": cty.StringVal("changed"),
  3679  					}),
  3680  				}),
  3681  				"nested_block_set": cty.ListVal([]cty.Value{
  3682  					cty.ObjectVal(map[string]cty.Value{
  3683  						"an_attr": cty.StringVal("changed"),
  3684  					}),
  3685  				}),
  3686  			}),
  3687  			BeforeValMarks: []cty.PathValueMarks{
  3688  				{
  3689  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  3690  					Marks: cty.NewValueMarks(marks.Sensitive),
  3691  				},
  3692  				{
  3693  					Path:  cty.Path{cty.GetAttrStep{Name: "special"}},
  3694  					Marks: cty.NewValueMarks(marks.Sensitive),
  3695  				},
  3696  				{
  3697  					Path:  cty.Path{cty.GetAttrStep{Name: "some_number"}},
  3698  					Marks: cty.NewValueMarks(marks.Sensitive),
  3699  				},
  3700  				{
  3701  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}},
  3702  					Marks: cty.NewValueMarks(marks.Sensitive),
  3703  				},
  3704  				{
  3705  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  3706  					Marks: cty.NewValueMarks(marks.Sensitive),
  3707  				},
  3708  				{
  3709  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  3710  					Marks: cty.NewValueMarks(marks.Sensitive),
  3711  				},
  3712  				{
  3713  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block"}},
  3714  					Marks: cty.NewValueMarks(marks.Sensitive),
  3715  				},
  3716  				{
  3717  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
  3718  					Marks: cty.NewValueMarks(marks.Sensitive),
  3719  				},
  3720  			},
  3721  			RequiredReplace: cty.NewPathSet(),
  3722  			Schema: &configschema.Block{
  3723  				Attributes: map[string]*configschema.Attribute{
  3724  					"id":          {Type: cty.String, Optional: true, Computed: true},
  3725  					"ami":         {Type: cty.String, Optional: true},
  3726  					"list_field":  {Type: cty.List(cty.String), Optional: true},
  3727  					"special":     {Type: cty.Bool, Optional: true},
  3728  					"some_number": {Type: cty.Number, Optional: true},
  3729  					"map_key":     {Type: cty.Map(cty.Number), Optional: true},
  3730  					"map_whole":   {Type: cty.Map(cty.String), Optional: true},
  3731  				},
  3732  				BlockTypes: map[string]*configschema.NestedBlock{
  3733  					"nested_block": {
  3734  						Block: configschema.Block{
  3735  							Attributes: map[string]*configschema.Attribute{
  3736  								"an_attr": {Type: cty.String, Optional: true},
  3737  							},
  3738  						},
  3739  						Nesting: configschema.NestingList,
  3740  					},
  3741  					"nested_block_set": {
  3742  						Block: configschema.Block{
  3743  							Attributes: map[string]*configschema.Attribute{
  3744  								"an_attr": {Type: cty.String, Optional: true},
  3745  							},
  3746  						},
  3747  						Nesting: configschema.NestingSet,
  3748  					},
  3749  				},
  3750  			},
  3751  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3752    ~ resource "test_instance" "example" {
  3753        # Warning: this attribute value will no longer be marked as sensitive
  3754        # after applying this change.
  3755        ~ ami         = (sensitive)
  3756          id          = "i-02ae66f368e8518a9"
  3757        ~ list_field  = [
  3758              # (1 unchanged element hidden)
  3759              "friends",
  3760            - (sensitive),
  3761            + ".",
  3762          ]
  3763        ~ map_key     = {
  3764            # Warning: this attribute value will no longer be marked as sensitive
  3765            # after applying this change.
  3766            ~ "dinner"    = (sensitive)
  3767              # (1 unchanged element hidden)
  3768          }
  3769        # Warning: this attribute value will no longer be marked as sensitive
  3770        # after applying this change.
  3771        ~ map_whole   = (sensitive)
  3772        # Warning: this attribute value will no longer be marked as sensitive
  3773        # after applying this change.
  3774        ~ some_number = (sensitive)
  3775        # Warning: this attribute value will no longer be marked as sensitive
  3776        # after applying this change.
  3777        ~ special     = (sensitive)
  3778  
  3779        # Warning: this block will no longer be marked as sensitive
  3780        # after applying this change.
  3781        ~ nested_block {
  3782            # At least one attribute in this block is (or was) sensitive,
  3783            # so its contents will not be displayed.
  3784          }
  3785  
  3786        # Warning: this block will no longer be marked as sensitive
  3787        # after applying this change.
  3788        ~ nested_block_set {
  3789            # At least one attribute in this block is (or was) sensitive,
  3790            # so its contents will not be displayed.
  3791          }
  3792      }
  3793  `,
  3794  		},
  3795  		"in-place update - after sensitive": {
  3796  			Action: plans.Update,
  3797  			Mode:   addrs.ManagedResourceMode,
  3798  			Before: cty.ObjectVal(map[string]cty.Value{
  3799  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  3800  				"list_field": cty.ListVal([]cty.Value{
  3801  					cty.StringVal("hello"),
  3802  					cty.StringVal("friends"),
  3803  				}),
  3804  				"map_key": cty.MapVal(map[string]cty.Value{
  3805  					"breakfast": cty.NumberIntVal(800),
  3806  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  3807  				}),
  3808  				"map_whole": cty.MapVal(map[string]cty.Value{
  3809  					"breakfast": cty.StringVal("pizza"),
  3810  					"dinner":    cty.StringVal("pizza"),
  3811  				}),
  3812  				"nested_block_single": cty.ObjectVal(map[string]cty.Value{
  3813  					"an_attr": cty.StringVal("original"),
  3814  				}),
  3815  			}),
  3816  			After: cty.ObjectVal(map[string]cty.Value{
  3817  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  3818  				"list_field": cty.ListVal([]cty.Value{
  3819  					cty.StringVal("goodbye"),
  3820  					cty.StringVal("friends"),
  3821  				}),
  3822  				"map_key": cty.MapVal(map[string]cty.Value{
  3823  					"breakfast": cty.NumberIntVal(700),
  3824  					"dinner":    cty.NumberIntVal(2100), // sensitive key
  3825  				}),
  3826  				"map_whole": cty.MapVal(map[string]cty.Value{
  3827  					"breakfast": cty.StringVal("cereal"),
  3828  					"dinner":    cty.StringVal("pizza"),
  3829  				}),
  3830  				"nested_block_single": cty.ObjectVal(map[string]cty.Value{
  3831  					"an_attr": cty.StringVal("changed"),
  3832  				}),
  3833  			}),
  3834  			AfterValMarks: []cty.PathValueMarks{
  3835  				{
  3836  					Path:  cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("address")}},
  3837  					Marks: cty.NewValueMarks(marks.Sensitive),
  3838  				},
  3839  				{
  3840  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
  3841  					Marks: cty.NewValueMarks(marks.Sensitive),
  3842  				},
  3843  				{
  3844  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  3845  					Marks: cty.NewValueMarks(marks.Sensitive),
  3846  				},
  3847  				{
  3848  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  3849  					Marks: cty.NewValueMarks(marks.Sensitive),
  3850  				},
  3851  				{
  3852  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_single"}},
  3853  					Marks: cty.NewValueMarks(marks.Sensitive),
  3854  				},
  3855  			},
  3856  			RequiredReplace: cty.NewPathSet(),
  3857  			Schema: &configschema.Block{
  3858  				Attributes: map[string]*configschema.Attribute{
  3859  					"id":         {Type: cty.String, Optional: true, Computed: true},
  3860  					"list_field": {Type: cty.List(cty.String), Optional: true},
  3861  					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
  3862  					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
  3863  				},
  3864  				BlockTypes: map[string]*configschema.NestedBlock{
  3865  					"nested_block_single": {
  3866  						Block: configschema.Block{
  3867  							Attributes: map[string]*configschema.Attribute{
  3868  								"an_attr": {Type: cty.String, Optional: true},
  3869  							},
  3870  						},
  3871  						Nesting: configschema.NestingSingle,
  3872  					},
  3873  				},
  3874  			},
  3875  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3876    ~ resource "test_instance" "example" {
  3877          id         = "i-02ae66f368e8518a9"
  3878        ~ list_field = [
  3879            - "hello",
  3880            + (sensitive),
  3881              "friends",
  3882          ]
  3883        ~ map_key    = {
  3884            ~ "breakfast" = 800 -> 700
  3885            # Warning: this attribute value will be marked as sensitive and will not
  3886            # display in UI output after applying this change.
  3887            ~ "dinner"    = (sensitive)
  3888          }
  3889        # Warning: this attribute value will be marked as sensitive and will not
  3890        # display in UI output after applying this change.
  3891        ~ map_whole  = (sensitive)
  3892  
  3893        # Warning: this block will be marked as sensitive and will not
  3894        # display in UI output after applying this change.
  3895        ~ nested_block_single {
  3896            # At least one attribute in this block is (or was) sensitive,
  3897            # so its contents will not be displayed.
  3898          }
  3899      }
  3900  `,
  3901  		},
  3902  		"in-place update - both sensitive": {
  3903  			Action: plans.Update,
  3904  			Mode:   addrs.ManagedResourceMode,
  3905  			Before: cty.ObjectVal(map[string]cty.Value{
  3906  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3907  				"ami": cty.StringVal("ami-BEFORE"),
  3908  				"list_field": cty.ListVal([]cty.Value{
  3909  					cty.StringVal("hello"),
  3910  					cty.StringVal("friends"),
  3911  				}),
  3912  				"map_key": cty.MapVal(map[string]cty.Value{
  3913  					"breakfast": cty.NumberIntVal(800),
  3914  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  3915  				}),
  3916  				"map_whole": cty.MapVal(map[string]cty.Value{
  3917  					"breakfast": cty.StringVal("pizza"),
  3918  					"dinner":    cty.StringVal("pizza"),
  3919  				}),
  3920  				"nested_block_map": cty.MapVal(map[string]cty.Value{
  3921  					"foo": cty.ObjectVal(map[string]cty.Value{
  3922  						"an_attr": cty.StringVal("original"),
  3923  					}),
  3924  				}),
  3925  			}),
  3926  			After: cty.ObjectVal(map[string]cty.Value{
  3927  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  3928  				"ami": cty.StringVal("ami-AFTER"),
  3929  				"list_field": cty.ListVal([]cty.Value{
  3930  					cty.StringVal("goodbye"),
  3931  					cty.StringVal("friends"),
  3932  				}),
  3933  				"map_key": cty.MapVal(map[string]cty.Value{
  3934  					"breakfast": cty.NumberIntVal(800),
  3935  					"dinner":    cty.NumberIntVal(1800), // sensitive key
  3936  				}),
  3937  				"map_whole": cty.MapVal(map[string]cty.Value{
  3938  					"breakfast": cty.StringVal("cereal"),
  3939  					"dinner":    cty.StringVal("pizza"),
  3940  				}),
  3941  				"nested_block_map": cty.MapVal(map[string]cty.Value{
  3942  					"foo": cty.ObjectVal(map[string]cty.Value{
  3943  						"an_attr": cty.UnknownVal(cty.String),
  3944  					}),
  3945  				}),
  3946  			}),
  3947  			BeforeValMarks: []cty.PathValueMarks{
  3948  				{
  3949  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  3950  					Marks: cty.NewValueMarks(marks.Sensitive),
  3951  				},
  3952  				{
  3953  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
  3954  					Marks: cty.NewValueMarks(marks.Sensitive),
  3955  				},
  3956  				{
  3957  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  3958  					Marks: cty.NewValueMarks(marks.Sensitive),
  3959  				},
  3960  				{
  3961  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  3962  					Marks: cty.NewValueMarks(marks.Sensitive),
  3963  				},
  3964  				{
  3965  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_map"}},
  3966  					Marks: cty.NewValueMarks(marks.Sensitive),
  3967  				},
  3968  			},
  3969  			AfterValMarks: []cty.PathValueMarks{
  3970  				{
  3971  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  3972  					Marks: cty.NewValueMarks(marks.Sensitive),
  3973  				},
  3974  				{
  3975  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
  3976  					Marks: cty.NewValueMarks(marks.Sensitive),
  3977  				},
  3978  				{
  3979  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  3980  					Marks: cty.NewValueMarks(marks.Sensitive),
  3981  				},
  3982  				{
  3983  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  3984  					Marks: cty.NewValueMarks(marks.Sensitive),
  3985  				},
  3986  				{
  3987  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_map"}},
  3988  					Marks: cty.NewValueMarks(marks.Sensitive),
  3989  				},
  3990  			},
  3991  			RequiredReplace: cty.NewPathSet(),
  3992  			Schema: &configschema.Block{
  3993  				Attributes: map[string]*configschema.Attribute{
  3994  					"id":         {Type: cty.String, Optional: true, Computed: true},
  3995  					"ami":        {Type: cty.String, Optional: true},
  3996  					"list_field": {Type: cty.List(cty.String), Optional: true},
  3997  					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
  3998  					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
  3999  				},
  4000  				BlockTypes: map[string]*configschema.NestedBlock{
  4001  					"nested_block_map": {
  4002  						Block: configschema.Block{
  4003  							Attributes: map[string]*configschema.Attribute{
  4004  								"an_attr": {Type: cty.String, Optional: true},
  4005  							},
  4006  						},
  4007  						Nesting: configschema.NestingMap,
  4008  					},
  4009  				},
  4010  			},
  4011  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4012    ~ resource "test_instance" "example" {
  4013        ~ ami        = (sensitive)
  4014          id         = "i-02ae66f368e8518a9"
  4015        ~ list_field = [
  4016            - (sensitive),
  4017            + (sensitive),
  4018              "friends",
  4019          ]
  4020        ~ map_key    = {
  4021            ~ "dinner"    = (sensitive)
  4022              # (1 unchanged element hidden)
  4023          }
  4024        ~ map_whole  = (sensitive)
  4025  
  4026        ~ nested_block_map {
  4027            # At least one attribute in this block is (or was) sensitive,
  4028            # so its contents will not be displayed.
  4029          }
  4030      }
  4031  `,
  4032  		},
  4033  		"in-place update - value unchanged, sensitivity changes": {
  4034  			Action: plans.Update,
  4035  			Mode:   addrs.ManagedResourceMode,
  4036  			Before: cty.ObjectVal(map[string]cty.Value{
  4037  				"id":          cty.StringVal("i-02ae66f368e8518a9"),
  4038  				"ami":         cty.StringVal("ami-BEFORE"),
  4039  				"special":     cty.BoolVal(true),
  4040  				"some_number": cty.NumberIntVal(1),
  4041  				"list_field": cty.ListVal([]cty.Value{
  4042  					cty.StringVal("hello"),
  4043  					cty.StringVal("friends"),
  4044  					cty.StringVal("!"),
  4045  				}),
  4046  				"map_key": cty.MapVal(map[string]cty.Value{
  4047  					"breakfast": cty.NumberIntVal(800),
  4048  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  4049  				}),
  4050  				"map_whole": cty.MapVal(map[string]cty.Value{
  4051  					"breakfast": cty.StringVal("pizza"),
  4052  					"dinner":    cty.StringVal("pizza"),
  4053  				}),
  4054  				"nested_block": cty.ListVal([]cty.Value{
  4055  					cty.ObjectVal(map[string]cty.Value{
  4056  						"an_attr": cty.StringVal("secretval"),
  4057  					}),
  4058  				}),
  4059  				"nested_block_set": cty.ListVal([]cty.Value{
  4060  					cty.ObjectVal(map[string]cty.Value{
  4061  						"an_attr": cty.StringVal("secretval"),
  4062  					}),
  4063  				}),
  4064  			}),
  4065  			After: cty.ObjectVal(map[string]cty.Value{
  4066  				"id":          cty.StringVal("i-02ae66f368e8518a9"),
  4067  				"ami":         cty.StringVal("ami-BEFORE"),
  4068  				"special":     cty.BoolVal(true),
  4069  				"some_number": cty.NumberIntVal(1),
  4070  				"list_field": cty.ListVal([]cty.Value{
  4071  					cty.StringVal("hello"),
  4072  					cty.StringVal("friends"),
  4073  					cty.StringVal("!"),
  4074  				}),
  4075  				"map_key": cty.MapVal(map[string]cty.Value{
  4076  					"breakfast": cty.NumberIntVal(800),
  4077  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  4078  				}),
  4079  				"map_whole": cty.MapVal(map[string]cty.Value{
  4080  					"breakfast": cty.StringVal("pizza"),
  4081  					"dinner":    cty.StringVal("pizza"),
  4082  				}),
  4083  				"nested_block": cty.ListVal([]cty.Value{
  4084  					cty.ObjectVal(map[string]cty.Value{
  4085  						"an_attr": cty.StringVal("secretval"),
  4086  					}),
  4087  				}),
  4088  				"nested_block_set": cty.ListVal([]cty.Value{
  4089  					cty.ObjectVal(map[string]cty.Value{
  4090  						"an_attr": cty.StringVal("secretval"),
  4091  					}),
  4092  				}),
  4093  			}),
  4094  			BeforeValMarks: []cty.PathValueMarks{
  4095  				{
  4096  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  4097  					Marks: cty.NewValueMarks(marks.Sensitive),
  4098  				},
  4099  				{
  4100  					Path:  cty.Path{cty.GetAttrStep{Name: "special"}},
  4101  					Marks: cty.NewValueMarks(marks.Sensitive),
  4102  				},
  4103  				{
  4104  					Path:  cty.Path{cty.GetAttrStep{Name: "some_number"}},
  4105  					Marks: cty.NewValueMarks(marks.Sensitive),
  4106  				},
  4107  				{
  4108  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}},
  4109  					Marks: cty.NewValueMarks(marks.Sensitive),
  4110  				},
  4111  				{
  4112  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  4113  					Marks: cty.NewValueMarks(marks.Sensitive),
  4114  				},
  4115  				{
  4116  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  4117  					Marks: cty.NewValueMarks(marks.Sensitive),
  4118  				},
  4119  				{
  4120  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block"}},
  4121  					Marks: cty.NewValueMarks(marks.Sensitive),
  4122  				},
  4123  				{
  4124  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
  4125  					Marks: cty.NewValueMarks(marks.Sensitive),
  4126  				},
  4127  			},
  4128  			RequiredReplace: cty.NewPathSet(),
  4129  			Schema: &configschema.Block{
  4130  				Attributes: map[string]*configschema.Attribute{
  4131  					"id":          {Type: cty.String, Optional: true, Computed: true},
  4132  					"ami":         {Type: cty.String, Optional: true},
  4133  					"list_field":  {Type: cty.List(cty.String), Optional: true},
  4134  					"special":     {Type: cty.Bool, Optional: true},
  4135  					"some_number": {Type: cty.Number, Optional: true},
  4136  					"map_key":     {Type: cty.Map(cty.Number), Optional: true},
  4137  					"map_whole":   {Type: cty.Map(cty.String), Optional: true},
  4138  				},
  4139  				BlockTypes: map[string]*configschema.NestedBlock{
  4140  					"nested_block": {
  4141  						Block: configschema.Block{
  4142  							Attributes: map[string]*configschema.Attribute{
  4143  								"an_attr": {Type: cty.String, Optional: true},
  4144  							},
  4145  						},
  4146  						Nesting: configschema.NestingList,
  4147  					},
  4148  					"nested_block_set": {
  4149  						Block: configschema.Block{
  4150  							Attributes: map[string]*configschema.Attribute{
  4151  								"an_attr": {Type: cty.String, Optional: true},
  4152  							},
  4153  						},
  4154  						Nesting: configschema.NestingSet,
  4155  					},
  4156  				},
  4157  			},
  4158  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4159    ~ resource "test_instance" "example" {
  4160        # Warning: this attribute value will no longer be marked as sensitive
  4161        # after applying this change. The value is unchanged.
  4162        ~ ami         = (sensitive)
  4163          id          = "i-02ae66f368e8518a9"
  4164        ~ list_field  = [
  4165              # (1 unchanged element hidden)
  4166              "friends",
  4167            - (sensitive),
  4168            + "!",
  4169          ]
  4170        ~ map_key     = {
  4171            # Warning: this attribute value will no longer be marked as sensitive
  4172            # after applying this change. The value is unchanged.
  4173            ~ "dinner"    = (sensitive)
  4174              # (1 unchanged element hidden)
  4175          }
  4176        # Warning: this attribute value will no longer be marked as sensitive
  4177        # after applying this change. The value is unchanged.
  4178        ~ map_whole   = (sensitive)
  4179        # Warning: this attribute value will no longer be marked as sensitive
  4180        # after applying this change. The value is unchanged.
  4181        ~ some_number = (sensitive)
  4182        # Warning: this attribute value will no longer be marked as sensitive
  4183        # after applying this change. The value is unchanged.
  4184        ~ special     = (sensitive)
  4185  
  4186        # Warning: this block will no longer be marked as sensitive
  4187        # after applying this change.
  4188        ~ nested_block {
  4189            # At least one attribute in this block is (or was) sensitive,
  4190            # so its contents will not be displayed.
  4191          }
  4192  
  4193        # Warning: this block will no longer be marked as sensitive
  4194        # after applying this change.
  4195        ~ nested_block_set {
  4196            # At least one attribute in this block is (or was) sensitive,
  4197            # so its contents will not be displayed.
  4198          }
  4199      }
  4200  `,
  4201  		},
  4202  		"deletion": {
  4203  			Action: plans.Delete,
  4204  			Mode:   addrs.ManagedResourceMode,
  4205  			Before: cty.ObjectVal(map[string]cty.Value{
  4206  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4207  				"ami": cty.StringVal("ami-BEFORE"),
  4208  				"list_field": cty.ListVal([]cty.Value{
  4209  					cty.StringVal("hello"),
  4210  					cty.StringVal("friends"),
  4211  				}),
  4212  				"map_key": cty.MapVal(map[string]cty.Value{
  4213  					"breakfast": cty.NumberIntVal(800),
  4214  					"dinner":    cty.NumberIntVal(2000), // sensitive key
  4215  				}),
  4216  				"map_whole": cty.MapVal(map[string]cty.Value{
  4217  					"breakfast": cty.StringVal("pizza"),
  4218  					"dinner":    cty.StringVal("pizza"),
  4219  				}),
  4220  				"nested_block": cty.ListVal([]cty.Value{
  4221  					cty.ObjectVal(map[string]cty.Value{
  4222  						"an_attr": cty.StringVal("secret"),
  4223  						"another": cty.StringVal("not secret"),
  4224  					}),
  4225  				}),
  4226  				"nested_block_set": cty.ListVal([]cty.Value{
  4227  					cty.ObjectVal(map[string]cty.Value{
  4228  						"an_attr": cty.StringVal("secret"),
  4229  						"another": cty.StringVal("not secret"),
  4230  					}),
  4231  				}),
  4232  			}),
  4233  			After: cty.NullVal(cty.EmptyObject),
  4234  			BeforeValMarks: []cty.PathValueMarks{
  4235  				{
  4236  					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
  4237  					Marks: cty.NewValueMarks(marks.Sensitive),
  4238  				},
  4239  				{
  4240  					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}},
  4241  					Marks: cty.NewValueMarks(marks.Sensitive),
  4242  				},
  4243  				{
  4244  					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
  4245  					Marks: cty.NewValueMarks(marks.Sensitive),
  4246  				},
  4247  				{
  4248  					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
  4249  					Marks: cty.NewValueMarks(marks.Sensitive),
  4250  				},
  4251  				{
  4252  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block"}},
  4253  					Marks: cty.NewValueMarks(marks.Sensitive),
  4254  				},
  4255  				{
  4256  					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
  4257  					Marks: cty.NewValueMarks(marks.Sensitive),
  4258  				},
  4259  			},
  4260  			RequiredReplace: cty.NewPathSet(),
  4261  			Schema: &configschema.Block{
  4262  				Attributes: map[string]*configschema.Attribute{
  4263  					"id":         {Type: cty.String, Optional: true, Computed: true},
  4264  					"ami":        {Type: cty.String, Optional: true},
  4265  					"list_field": {Type: cty.List(cty.String), Optional: true},
  4266  					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
  4267  					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
  4268  				},
  4269  				BlockTypes: map[string]*configschema.NestedBlock{
  4270  					"nested_block_set": {
  4271  						Block: configschema.Block{
  4272  							Attributes: map[string]*configschema.Attribute{
  4273  								"an_attr": {Type: cty.String, Optional: true},
  4274  								"another": {Type: cty.String, Optional: true},
  4275  							},
  4276  						},
  4277  						Nesting: configschema.NestingSet,
  4278  					},
  4279  				},
  4280  			},
  4281  			ExpectedOutput: `  # test_instance.example will be destroyed
  4282    - resource "test_instance" "example" {
  4283        - ami        = (sensitive) -> null
  4284        - id         = "i-02ae66f368e8518a9" -> null
  4285        - list_field = [
  4286            - "hello",
  4287            - (sensitive),
  4288          ] -> null
  4289        - map_key    = {
  4290            - "breakfast" = 800
  4291            - "dinner"    = (sensitive)
  4292          } -> null
  4293        - map_whole  = (sensitive) -> null
  4294  
  4295        - nested_block_set {
  4296            # At least one attribute in this block is (or was) sensitive,
  4297            # so its contents will not be displayed.
  4298          }
  4299      }
  4300  `,
  4301  		},
  4302  		"update with sensitive value forcing replacement": {
  4303  			Action: plans.DeleteThenCreate,
  4304  			Mode:   addrs.ManagedResourceMode,
  4305  			Before: cty.ObjectVal(map[string]cty.Value{
  4306  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4307  				"ami": cty.StringVal("ami-BEFORE"),
  4308  				"nested_block_set": cty.SetVal([]cty.Value{
  4309  					cty.ObjectVal(map[string]cty.Value{
  4310  						"an_attr": cty.StringVal("secret"),
  4311  					}),
  4312  				}),
  4313  			}),
  4314  			After: cty.ObjectVal(map[string]cty.Value{
  4315  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4316  				"ami": cty.StringVal("ami-AFTER"),
  4317  				"nested_block_set": cty.SetVal([]cty.Value{
  4318  					cty.ObjectVal(map[string]cty.Value{
  4319  						"an_attr": cty.StringVal("changed"),
  4320  					}),
  4321  				}),
  4322  			}),
  4323  			BeforeValMarks: []cty.PathValueMarks{
  4324  				{
  4325  					Path:  cty.GetAttrPath("ami"),
  4326  					Marks: cty.NewValueMarks(marks.Sensitive),
  4327  				},
  4328  				{
  4329  					Path:  cty.GetAttrPath("nested_block_set"),
  4330  					Marks: cty.NewValueMarks(marks.Sensitive),
  4331  				},
  4332  			},
  4333  			AfterValMarks: []cty.PathValueMarks{
  4334  				{
  4335  					Path:  cty.GetAttrPath("ami"),
  4336  					Marks: cty.NewValueMarks(marks.Sensitive),
  4337  				},
  4338  				{
  4339  					Path:  cty.GetAttrPath("nested_block_set"),
  4340  					Marks: cty.NewValueMarks(marks.Sensitive),
  4341  				},
  4342  			},
  4343  			Schema: &configschema.Block{
  4344  				Attributes: map[string]*configschema.Attribute{
  4345  					"id":  {Type: cty.String, Optional: true, Computed: true},
  4346  					"ami": {Type: cty.String, Optional: true},
  4347  				},
  4348  				BlockTypes: map[string]*configschema.NestedBlock{
  4349  					"nested_block_set": {
  4350  						Block: configschema.Block{
  4351  							Attributes: map[string]*configschema.Attribute{
  4352  								"an_attr": {Type: cty.String, Required: true},
  4353  							},
  4354  						},
  4355  						Nesting: configschema.NestingSet,
  4356  					},
  4357  				},
  4358  			},
  4359  			RequiredReplace: cty.NewPathSet(
  4360  				cty.GetAttrPath("ami"),
  4361  				cty.GetAttrPath("nested_block_set"),
  4362  			),
  4363  			ExpectedOutput: `  # test_instance.example must be replaced
  4364  -/+ resource "test_instance" "example" {
  4365        ~ ami = (sensitive) # forces replacement
  4366          id  = "i-02ae66f368e8518a9"
  4367  
  4368        ~ nested_block_set { # forces replacement
  4369            # At least one attribute in this block is (or was) sensitive,
  4370            # so its contents will not be displayed.
  4371          }
  4372      }
  4373  `,
  4374  		},
  4375  		"update with sensitive attribute forcing replacement": {
  4376  			Action: plans.DeleteThenCreate,
  4377  			Mode:   addrs.ManagedResourceMode,
  4378  			Before: cty.ObjectVal(map[string]cty.Value{
  4379  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4380  				"ami": cty.StringVal("ami-BEFORE"),
  4381  			}),
  4382  			After: cty.ObjectVal(map[string]cty.Value{
  4383  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  4384  				"ami": cty.StringVal("ami-AFTER"),
  4385  			}),
  4386  			Schema: &configschema.Block{
  4387  				Attributes: map[string]*configschema.Attribute{
  4388  					"id":  {Type: cty.String, Optional: true, Computed: true},
  4389  					"ami": {Type: cty.String, Optional: true, Computed: true, Sensitive: true},
  4390  				},
  4391  			},
  4392  			RequiredReplace: cty.NewPathSet(
  4393  				cty.GetAttrPath("ami"),
  4394  			),
  4395  			ExpectedOutput: `  # test_instance.example must be replaced
  4396  -/+ resource "test_instance" "example" {
  4397        ~ ami = (sensitive value) # forces replacement
  4398          id  = "i-02ae66f368e8518a9"
  4399      }
  4400  `,
  4401  		},
  4402  		"update with sensitive nested type attribute forcing replacement": {
  4403  			Action: plans.DeleteThenCreate,
  4404  			Mode:   addrs.ManagedResourceMode,
  4405  			Before: cty.ObjectVal(map[string]cty.Value{
  4406  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  4407  				"conn_info": cty.ObjectVal(map[string]cty.Value{
  4408  					"user":     cty.StringVal("not-secret"),
  4409  					"password": cty.StringVal("top-secret"),
  4410  				}),
  4411  			}),
  4412  			After: cty.ObjectVal(map[string]cty.Value{
  4413  				"id": cty.StringVal("i-02ae66f368e8518a9"),
  4414  				"conn_info": cty.ObjectVal(map[string]cty.Value{
  4415  					"user":     cty.StringVal("not-secret"),
  4416  					"password": cty.StringVal("new-secret"),
  4417  				}),
  4418  			}),
  4419  			Schema: &configschema.Block{
  4420  				Attributes: map[string]*configschema.Attribute{
  4421  					"id": {Type: cty.String, Optional: true, Computed: true},
  4422  					"conn_info": {
  4423  						NestedType: &configschema.Object{
  4424  							Nesting: configschema.NestingSingle,
  4425  							Attributes: map[string]*configschema.Attribute{
  4426  								"user":     {Type: cty.String, Optional: true},
  4427  								"password": {Type: cty.String, Optional: true, Sensitive: true},
  4428  							},
  4429  						},
  4430  					},
  4431  				},
  4432  			},
  4433  			RequiredReplace: cty.NewPathSet(
  4434  				cty.GetAttrPath("conn_info"),
  4435  				cty.GetAttrPath("password"),
  4436  			),
  4437  			ExpectedOutput: `  # test_instance.example must be replaced
  4438  -/+ resource "test_instance" "example" {
  4439        ~ conn_info = { # forces replacement
  4440          ~ password = (sensitive value)
  4441            # (1 unchanged attribute hidden)
  4442        }
  4443          id        = "i-02ae66f368e8518a9"
  4444      }
  4445  `,
  4446  		},
  4447  	}
  4448  	runTestCases(t, testCases)
  4449  }
  4450  
  4451  func TestResourceChange_moved(t *testing.T) {
  4452  	prevRunAddr := addrs.Resource{
  4453  		Mode: addrs.ManagedResourceMode,
  4454  		Type: "test_instance",
  4455  		Name: "previous",
  4456  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
  4457  
  4458  	testCases := map[string]testCase{
  4459  		"moved and updated": {
  4460  			PrevRunAddr: prevRunAddr,
  4461  			Action:      plans.Update,
  4462  			Mode:        addrs.ManagedResourceMode,
  4463  			Before: cty.ObjectVal(map[string]cty.Value{
  4464  				"id":  cty.StringVal("12345"),
  4465  				"foo": cty.StringVal("hello"),
  4466  				"bar": cty.StringVal("baz"),
  4467  			}),
  4468  			After: cty.ObjectVal(map[string]cty.Value{
  4469  				"id":  cty.StringVal("12345"),
  4470  				"foo": cty.StringVal("hello"),
  4471  				"bar": cty.StringVal("boop"),
  4472  			}),
  4473  			Schema: &configschema.Block{
  4474  				Attributes: map[string]*configschema.Attribute{
  4475  					"id":  {Type: cty.String, Computed: true},
  4476  					"foo": {Type: cty.String, Optional: true},
  4477  					"bar": {Type: cty.String, Optional: true},
  4478  				},
  4479  			},
  4480  			RequiredReplace: cty.NewPathSet(),
  4481  			ExpectedOutput: `  # test_instance.example will be updated in-place
  4482    # (test_instance.previous has moved to test_instance.example)
  4483    ~ resource "test_instance" "example" {
  4484        ~ bar = "baz" -> "boop"
  4485          id  = "12345"
  4486          # (1 unchanged attribute hidden)
  4487      }
  4488  `,
  4489  		},
  4490  		"moved without changes": {
  4491  			PrevRunAddr: prevRunAddr,
  4492  			Action:      plans.NoOp,
  4493  			Mode:        addrs.ManagedResourceMode,
  4494  			Before: cty.ObjectVal(map[string]cty.Value{
  4495  				"id":  cty.StringVal("12345"),
  4496  				"foo": cty.StringVal("hello"),
  4497  				"bar": cty.StringVal("baz"),
  4498  			}),
  4499  			After: cty.ObjectVal(map[string]cty.Value{
  4500  				"id":  cty.StringVal("12345"),
  4501  				"foo": cty.StringVal("hello"),
  4502  				"bar": cty.StringVal("baz"),
  4503  			}),
  4504  			Schema: &configschema.Block{
  4505  				Attributes: map[string]*configschema.Attribute{
  4506  					"id":  {Type: cty.String, Computed: true},
  4507  					"foo": {Type: cty.String, Optional: true},
  4508  					"bar": {Type: cty.String, Optional: true},
  4509  				},
  4510  			},
  4511  			RequiredReplace: cty.NewPathSet(),
  4512  			ExpectedOutput: `  # test_instance.previous has moved to test_instance.example
  4513      resource "test_instance" "example" {
  4514          id  = "12345"
  4515          # (2 unchanged attributes hidden)
  4516      }
  4517  `,
  4518  		},
  4519  	}
  4520  
  4521  	runTestCases(t, testCases)
  4522  }
  4523  
  4524  type testCase struct {
  4525  	Action          plans.Action
  4526  	ActionReason    plans.ResourceInstanceChangeActionReason
  4527  	Mode            addrs.ResourceMode
  4528  	DeposedKey      states.DeposedKey
  4529  	Before          cty.Value
  4530  	BeforeValMarks  []cty.PathValueMarks
  4531  	AfterValMarks   []cty.PathValueMarks
  4532  	After           cty.Value
  4533  	Schema          *configschema.Block
  4534  	RequiredReplace cty.PathSet
  4535  	ExpectedOutput  string
  4536  	PrevRunAddr     addrs.AbsResourceInstance
  4537  }
  4538  
  4539  func runTestCases(t *testing.T, testCases map[string]testCase) {
  4540  	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
  4541  
  4542  	for name, tc := range testCases {
  4543  		t.Run(name, func(t *testing.T) {
  4544  			ty := tc.Schema.ImpliedType()
  4545  
  4546  			beforeVal := tc.Before
  4547  			switch { // Some fixups to make the test cases a little easier to write
  4548  			case beforeVal.IsNull():
  4549  				beforeVal = cty.NullVal(ty) // allow mistyped nulls
  4550  			case !beforeVal.IsKnown():
  4551  				beforeVal = cty.UnknownVal(ty) // allow mistyped unknowns
  4552  			}
  4553  			before, err := plans.NewDynamicValue(beforeVal, ty)
  4554  			if err != nil {
  4555  				t.Fatal(err)
  4556  			}
  4557  
  4558  			afterVal := tc.After
  4559  			switch { // Some fixups to make the test cases a little easier to write
  4560  			case afterVal.IsNull():
  4561  				afterVal = cty.NullVal(ty) // allow mistyped nulls
  4562  			case !afterVal.IsKnown():
  4563  				afterVal = cty.UnknownVal(ty) // allow mistyped unknowns
  4564  			}
  4565  			after, err := plans.NewDynamicValue(afterVal, ty)
  4566  			if err != nil {
  4567  				t.Fatal(err)
  4568  			}
  4569  
  4570  			addr := addrs.Resource{
  4571  				Mode: tc.Mode,
  4572  				Type: "test_instance",
  4573  				Name: "example",
  4574  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
  4575  
  4576  			prevRunAddr := tc.PrevRunAddr
  4577  			// If no previous run address is given, reuse the current address
  4578  			// to make initialization easier
  4579  			if prevRunAddr.Resource.Resource.Type == "" {
  4580  				prevRunAddr = addr
  4581  			}
  4582  
  4583  			change := &plans.ResourceInstanceChangeSrc{
  4584  				Addr:        addr,
  4585  				PrevRunAddr: prevRunAddr,
  4586  				DeposedKey:  tc.DeposedKey,
  4587  				ProviderAddr: addrs.AbsProviderConfig{
  4588  					Provider: addrs.NewDefaultProvider("test"),
  4589  					Module:   addrs.RootModule,
  4590  				},
  4591  				ChangeSrc: plans.ChangeSrc{
  4592  					Action:         tc.Action,
  4593  					Before:         before,
  4594  					After:          after,
  4595  					BeforeValMarks: tc.BeforeValMarks,
  4596  					AfterValMarks:  tc.AfterValMarks,
  4597  				},
  4598  				ActionReason:    tc.ActionReason,
  4599  				RequiredReplace: tc.RequiredReplace,
  4600  			}
  4601  
  4602  			output := ResourceChange(change, tc.Schema, color, DiffLanguageProposedChange)
  4603  			if diff := cmp.Diff(output, tc.ExpectedOutput); diff != "" {
  4604  				t.Errorf("wrong output\n%s", diff)
  4605  			}
  4606  		})
  4607  	}
  4608  }
  4609  
  4610  func TestOutputChanges(t *testing.T) {
  4611  	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
  4612  
  4613  	testCases := map[string]struct {
  4614  		changes []*plans.OutputChangeSrc
  4615  		output  string
  4616  	}{
  4617  		"new output value": {
  4618  			[]*plans.OutputChangeSrc{
  4619  				outputChange(
  4620  					"foo",
  4621  					cty.NullVal(cty.DynamicPseudoType),
  4622  					cty.StringVal("bar"),
  4623  					false,
  4624  				),
  4625  			},
  4626  			`
  4627    + foo = "bar"`,
  4628  		},
  4629  		"removed output": {
  4630  			[]*plans.OutputChangeSrc{
  4631  				outputChange(
  4632  					"foo",
  4633  					cty.StringVal("bar"),
  4634  					cty.NullVal(cty.DynamicPseudoType),
  4635  					false,
  4636  				),
  4637  			},
  4638  			`
  4639    - foo = "bar" -> null`,
  4640  		},
  4641  		"single string change": {
  4642  			[]*plans.OutputChangeSrc{
  4643  				outputChange(
  4644  					"foo",
  4645  					cty.StringVal("bar"),
  4646  					cty.StringVal("baz"),
  4647  					false,
  4648  				),
  4649  			},
  4650  			`
  4651    ~ foo = "bar" -> "baz"`,
  4652  		},
  4653  		"element added to list": {
  4654  			[]*plans.OutputChangeSrc{
  4655  				outputChange(
  4656  					"foo",
  4657  					cty.ListVal([]cty.Value{
  4658  						cty.StringVal("alpha"),
  4659  						cty.StringVal("beta"),
  4660  						cty.StringVal("delta"),
  4661  						cty.StringVal("epsilon"),
  4662  					}),
  4663  					cty.ListVal([]cty.Value{
  4664  						cty.StringVal("alpha"),
  4665  						cty.StringVal("beta"),
  4666  						cty.StringVal("gamma"),
  4667  						cty.StringVal("delta"),
  4668  						cty.StringVal("epsilon"),
  4669  					}),
  4670  					false,
  4671  				),
  4672  			},
  4673  			`
  4674    ~ foo = [
  4675          # (1 unchanged element hidden)
  4676          "beta",
  4677        + "gamma",
  4678          "delta",
  4679          # (1 unchanged element hidden)
  4680      ]`,
  4681  		},
  4682  		"multiple outputs changed, one sensitive": {
  4683  			[]*plans.OutputChangeSrc{
  4684  				outputChange(
  4685  					"a",
  4686  					cty.NumberIntVal(1),
  4687  					cty.NumberIntVal(2),
  4688  					false,
  4689  				),
  4690  				outputChange(
  4691  					"b",
  4692  					cty.StringVal("hunter2"),
  4693  					cty.StringVal("correct-horse-battery-staple"),
  4694  					true,
  4695  				),
  4696  				outputChange(
  4697  					"c",
  4698  					cty.BoolVal(false),
  4699  					cty.BoolVal(true),
  4700  					false,
  4701  				),
  4702  			},
  4703  			`
  4704    ~ a = 1 -> 2
  4705    ~ b = (sensitive value)
  4706    ~ c = false -> true`,
  4707  		},
  4708  	}
  4709  
  4710  	for name, tc := range testCases {
  4711  		t.Run(name, func(t *testing.T) {
  4712  			output := OutputChanges(tc.changes, color)
  4713  			if output != tc.output {
  4714  				t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.output)
  4715  			}
  4716  		})
  4717  	}
  4718  }
  4719  
  4720  func outputChange(name string, before, after cty.Value, sensitive bool) *plans.OutputChangeSrc {
  4721  	addr := addrs.AbsOutputValue{
  4722  		OutputValue: addrs.OutputValue{Name: name},
  4723  	}
  4724  
  4725  	change := &plans.OutputChange{
  4726  		Addr: addr, Change: plans.Change{
  4727  			Before: before,
  4728  			After:  after,
  4729  		},
  4730  		Sensitive: sensitive,
  4731  	}
  4732  
  4733  	changeSrc, err := change.Encode()
  4734  	if err != nil {
  4735  		panic(fmt.Sprintf("failed to encode change for %s: %s", addr, err))
  4736  	}
  4737  
  4738  	return changeSrc
  4739  }
  4740  
  4741  // A basic test schema using a configurable NestingMode for one (NestedType) attribute and one block
  4742  func testSchema(nesting configschema.NestingMode) *configschema.Block {
  4743  	return &configschema.Block{
  4744  		Attributes: map[string]*configschema.Attribute{
  4745  			"id":  {Type: cty.String, Optional: true, Computed: true},
  4746  			"ami": {Type: cty.String, Optional: true},
  4747  			"disks": {
  4748  				NestedType: &configschema.Object{
  4749  					Attributes: map[string]*configschema.Attribute{
  4750  						"mount_point": {Type: cty.String, Optional: true},
  4751  						"size":        {Type: cty.String, Optional: true},
  4752  					},
  4753  					Nesting: nesting,
  4754  				},
  4755  			},
  4756  		},
  4757  		BlockTypes: map[string]*configschema.NestedBlock{
  4758  			"root_block_device": {
  4759  				Block: configschema.Block{
  4760  					Attributes: map[string]*configschema.Attribute{
  4761  						"volume_type": {
  4762  							Type:     cty.String,
  4763  							Optional: true,
  4764  							Computed: true,
  4765  						},
  4766  					},
  4767  				},
  4768  				Nesting: nesting,
  4769  			},
  4770  		},
  4771  	}
  4772  }
  4773  
  4774  // similar to testSchema with the addition of a "new_field" block
  4775  func testSchemaPlus(nesting configschema.NestingMode) *configschema.Block {
  4776  	return &configschema.Block{
  4777  		Attributes: map[string]*configschema.Attribute{
  4778  			"id":  {Type: cty.String, Optional: true, Computed: true},
  4779  			"ami": {Type: cty.String, Optional: true},
  4780  			"disks": {
  4781  				NestedType: &configschema.Object{
  4782  					Attributes: map[string]*configschema.Attribute{
  4783  						"mount_point": {Type: cty.String, Optional: true},
  4784  						"size":        {Type: cty.String, Optional: true},
  4785  					},
  4786  					Nesting: nesting,
  4787  				},
  4788  			},
  4789  		},
  4790  		BlockTypes: map[string]*configschema.NestedBlock{
  4791  			"root_block_device": {
  4792  				Block: configschema.Block{
  4793  					Attributes: map[string]*configschema.Attribute{
  4794  						"volume_type": {
  4795  							Type:     cty.String,
  4796  							Optional: true,
  4797  							Computed: true,
  4798  						},
  4799  						"new_field": {
  4800  							Type:     cty.String,
  4801  							Optional: true,
  4802  							Computed: true,
  4803  						},
  4804  					},
  4805  				},
  4806  				Nesting: nesting,
  4807  			},
  4808  		},
  4809  	}
  4810  }