github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/command/format/diff_test.go (about)

     1  package format
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
     7  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema"
     8  	"github.com/hashicorp/terraform-plugin-sdk/internal/plans"
     9  	"github.com/mitchellh/colorstring"
    10  	"github.com/zclconf/go-cty/cty"
    11  )
    12  
    13  func TestResourceChange_primitiveTypes(t *testing.T) {
    14  	testCases := map[string]testCase{
    15  		"creation": {
    16  			Action: plans.Create,
    17  			Mode:   addrs.ManagedResourceMode,
    18  			Before: cty.NullVal(cty.EmptyObject),
    19  			After: cty.ObjectVal(map[string]cty.Value{
    20  				"id": cty.UnknownVal(cty.String),
    21  			}),
    22  			Schema: &configschema.Block{
    23  				Attributes: map[string]*configschema.Attribute{
    24  					"id": {Type: cty.String, Computed: true},
    25  				},
    26  			},
    27  			RequiredReplace: cty.NewPathSet(),
    28  			Tainted:         false,
    29  			ExpectedOutput: `  # test_instance.example will be created
    30    + resource "test_instance" "example" {
    31        + id = (known after apply)
    32      }
    33  `,
    34  		},
    35  		"creation (null string)": {
    36  			Action: plans.Create,
    37  			Mode:   addrs.ManagedResourceMode,
    38  			Before: cty.NullVal(cty.EmptyObject),
    39  			After: cty.ObjectVal(map[string]cty.Value{
    40  				"string": cty.StringVal("null"),
    41  			}),
    42  			Schema: &configschema.Block{
    43  				Attributes: map[string]*configschema.Attribute{
    44  					"string": {Type: cty.String, Optional: true},
    45  				},
    46  			},
    47  			RequiredReplace: cty.NewPathSet(),
    48  			Tainted:         false,
    49  			ExpectedOutput: `  # test_instance.example will be created
    50    + resource "test_instance" "example" {
    51        + string = "null"
    52      }
    53  `,
    54  		},
    55  		"deletion": {
    56  			Action: plans.Delete,
    57  			Mode:   addrs.ManagedResourceMode,
    58  			Before: cty.ObjectVal(map[string]cty.Value{
    59  				"id": cty.StringVal("i-02ae66f368e8518a9"),
    60  			}),
    61  			After: cty.NullVal(cty.EmptyObject),
    62  			Schema: &configschema.Block{
    63  				Attributes: map[string]*configschema.Attribute{
    64  					"id": {Type: cty.String, Computed: true},
    65  				},
    66  			},
    67  			RequiredReplace: cty.NewPathSet(),
    68  			Tainted:         false,
    69  			ExpectedOutput: `  # test_instance.example will be destroyed
    70    - resource "test_instance" "example" {
    71        - id = "i-02ae66f368e8518a9" -> null
    72      }
    73  `,
    74  		},
    75  		"deletion (empty string)": {
    76  			Action: plans.Delete,
    77  			Mode:   addrs.ManagedResourceMode,
    78  			Before: cty.ObjectVal(map[string]cty.Value{
    79  				"id":                 cty.StringVal("i-02ae66f368e8518a9"),
    80  				"intentionally_long": cty.StringVal(""),
    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  					"intentionally_long": {Type: cty.String, Optional: true},
    87  				},
    88  			},
    89  			RequiredReplace: cty.NewPathSet(),
    90  			Tainted:         false,
    91  			ExpectedOutput: `  # test_instance.example will be destroyed
    92    - resource "test_instance" "example" {
    93        - id = "i-02ae66f368e8518a9" -> null
    94      }
    95  `,
    96  		},
    97  		"string in-place update": {
    98  			Action: plans.Update,
    99  			Mode:   addrs.ManagedResourceMode,
   100  			Before: cty.ObjectVal(map[string]cty.Value{
   101  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   102  				"ami": cty.StringVal("ami-BEFORE"),
   103  			}),
   104  			After: cty.ObjectVal(map[string]cty.Value{
   105  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   106  				"ami": cty.StringVal("ami-AFTER"),
   107  			}),
   108  			Schema: &configschema.Block{
   109  				Attributes: map[string]*configschema.Attribute{
   110  					"id":  {Type: cty.String, Optional: true, Computed: true},
   111  					"ami": {Type: cty.String, Optional: true},
   112  				},
   113  			},
   114  			RequiredReplace: cty.NewPathSet(),
   115  			Tainted:         false,
   116  			ExpectedOutput: `  # test_instance.example will be updated in-place
   117    ~ resource "test_instance" "example" {
   118        ~ ami = "ami-BEFORE" -> "ami-AFTER"
   119          id  = "i-02ae66f368e8518a9"
   120      }
   121  `,
   122  		},
   123  		"string force-new update": {
   124  			Action: plans.DeleteThenCreate,
   125  			Mode:   addrs.ManagedResourceMode,
   126  			Before: cty.ObjectVal(map[string]cty.Value{
   127  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   128  				"ami": cty.StringVal("ami-BEFORE"),
   129  			}),
   130  			After: cty.ObjectVal(map[string]cty.Value{
   131  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   132  				"ami": cty.StringVal("ami-AFTER"),
   133  			}),
   134  			Schema: &configschema.Block{
   135  				Attributes: map[string]*configschema.Attribute{
   136  					"id":  {Type: cty.String, Optional: true, Computed: true},
   137  					"ami": {Type: cty.String, Optional: true},
   138  				},
   139  			},
   140  			RequiredReplace: cty.NewPathSet(cty.Path{
   141  				cty.GetAttrStep{Name: "ami"},
   142  			}),
   143  			Tainted: false,
   144  			ExpectedOutput: `  # test_instance.example must be replaced
   145  -/+ resource "test_instance" "example" {
   146        ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement
   147          id  = "i-02ae66f368e8518a9"
   148      }
   149  `,
   150  		},
   151  		"string in-place update (null values)": {
   152  			Action: plans.Update,
   153  			Mode:   addrs.ManagedResourceMode,
   154  			Before: cty.ObjectVal(map[string]cty.Value{
   155  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
   156  				"ami":       cty.StringVal("ami-BEFORE"),
   157  				"unchanged": cty.NullVal(cty.String),
   158  			}),
   159  			After: cty.ObjectVal(map[string]cty.Value{
   160  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
   161  				"ami":       cty.StringVal("ami-AFTER"),
   162  				"unchanged": cty.NullVal(cty.String),
   163  			}),
   164  			Schema: &configschema.Block{
   165  				Attributes: map[string]*configschema.Attribute{
   166  					"id":        {Type: cty.String, Optional: true, Computed: true},
   167  					"ami":       {Type: cty.String, Optional: true},
   168  					"unchanged": {Type: cty.String, Optional: true},
   169  				},
   170  			},
   171  			RequiredReplace: cty.NewPathSet(),
   172  			Tainted:         false,
   173  			ExpectedOutput: `  # test_instance.example will be updated in-place
   174    ~ resource "test_instance" "example" {
   175        ~ ami = "ami-BEFORE" -> "ami-AFTER"
   176          id  = "i-02ae66f368e8518a9"
   177      }
   178  `,
   179  		},
   180  		"in-place update of multi-line string field": {
   181  			Action: plans.Update,
   182  			Mode:   addrs.ManagedResourceMode,
   183  			Before: cty.ObjectVal(map[string]cty.Value{
   184  				"id": cty.StringVal("i-02ae66f368e8518a9"),
   185  				"more_lines": cty.StringVal(`original
   186  `),
   187  			}),
   188  			After: cty.ObjectVal(map[string]cty.Value{
   189  				"id": cty.UnknownVal(cty.String),
   190  				"more_lines": cty.StringVal(`original
   191  new line
   192  `),
   193  			}),
   194  			Schema: &configschema.Block{
   195  				Attributes: map[string]*configschema.Attribute{
   196  					"id":         {Type: cty.String, Optional: true, Computed: true},
   197  					"more_lines": {Type: cty.String, Optional: true},
   198  				},
   199  			},
   200  			RequiredReplace: cty.NewPathSet(),
   201  			Tainted:         false,
   202  			ExpectedOutput: `  # test_instance.example will be updated in-place
   203    ~ resource "test_instance" "example" {
   204        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   205        ~ more_lines = <<~EOT
   206              original
   207            + new line
   208          EOT
   209      }
   210  `,
   211  		},
   212  		"force-new update of multi-line string field": {
   213  			Action: plans.DeleteThenCreate,
   214  			Mode:   addrs.ManagedResourceMode,
   215  			Before: cty.ObjectVal(map[string]cty.Value{
   216  				"id": cty.StringVal("i-02ae66f368e8518a9"),
   217  				"more_lines": cty.StringVal(`original
   218  `),
   219  			}),
   220  			After: cty.ObjectVal(map[string]cty.Value{
   221  				"id": cty.UnknownVal(cty.String),
   222  				"more_lines": cty.StringVal(`original
   223  new line
   224  `),
   225  			}),
   226  			Schema: &configschema.Block{
   227  				Attributes: map[string]*configschema.Attribute{
   228  					"id":         {Type: cty.String, Optional: true, Computed: true},
   229  					"more_lines": {Type: cty.String, Optional: true},
   230  				},
   231  			},
   232  			RequiredReplace: cty.NewPathSet(cty.Path{
   233  				cty.GetAttrStep{Name: "more_lines"},
   234  			}),
   235  			Tainted: false,
   236  			ExpectedOutput: `  # test_instance.example must be replaced
   237  -/+ resource "test_instance" "example" {
   238        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   239        ~ more_lines = <<~EOT # forces replacement
   240              original
   241            + new line
   242          EOT
   243      }
   244  `,
   245  		},
   246  
   247  		// Sensitive
   248  
   249  		"creation with sensitive field": {
   250  			Action: plans.Create,
   251  			Mode:   addrs.ManagedResourceMode,
   252  			Before: cty.NullVal(cty.EmptyObject),
   253  			After: cty.ObjectVal(map[string]cty.Value{
   254  				"id":       cty.UnknownVal(cty.String),
   255  				"password": cty.StringVal("top-secret"),
   256  			}),
   257  			Schema: &configschema.Block{
   258  				Attributes: map[string]*configschema.Attribute{
   259  					"id":       {Type: cty.String, Computed: true},
   260  					"password": {Type: cty.String, Optional: true, Sensitive: true},
   261  				},
   262  			},
   263  			RequiredReplace: cty.NewPathSet(),
   264  			Tainted:         false,
   265  			ExpectedOutput: `  # test_instance.example will be created
   266    + resource "test_instance" "example" {
   267        + id       = (known after apply)
   268        + password = (sensitive value)
   269      }
   270  `,
   271  		},
   272  		"update with equal sensitive field": {
   273  			Action: plans.Update,
   274  			Mode:   addrs.ManagedResourceMode,
   275  			Before: cty.ObjectVal(map[string]cty.Value{
   276  				"id":       cty.StringVal("blah"),
   277  				"str":      cty.StringVal("before"),
   278  				"password": cty.StringVal("top-secret"),
   279  			}),
   280  			After: cty.ObjectVal(map[string]cty.Value{
   281  				"id":       cty.UnknownVal(cty.String),
   282  				"str":      cty.StringVal("after"),
   283  				"password": cty.StringVal("top-secret"),
   284  			}),
   285  			Schema: &configschema.Block{
   286  				Attributes: map[string]*configschema.Attribute{
   287  					"id":       {Type: cty.String, Computed: true},
   288  					"str":      {Type: cty.String, Optional: true},
   289  					"password": {Type: cty.String, Optional: true, Sensitive: true},
   290  				},
   291  			},
   292  			RequiredReplace: cty.NewPathSet(),
   293  			Tainted:         false,
   294  			ExpectedOutput: `  # test_instance.example will be updated in-place
   295    ~ resource "test_instance" "example" {
   296        ~ id       = "blah" -> (known after apply)
   297          password = (sensitive value)
   298        ~ str      = "before" -> "after"
   299      }
   300  `,
   301  		},
   302  
   303  		// tainted resources
   304  		"replace tainted resource": {
   305  			Action: plans.DeleteThenCreate,
   306  			Mode:   addrs.ManagedResourceMode,
   307  			Before: cty.ObjectVal(map[string]cty.Value{
   308  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
   309  				"ami": cty.StringVal("ami-BEFORE"),
   310  			}),
   311  			After: cty.ObjectVal(map[string]cty.Value{
   312  				"id":  cty.UnknownVal(cty.String),
   313  				"ami": cty.StringVal("ami-AFTER"),
   314  			}),
   315  			Schema: &configschema.Block{
   316  				Attributes: map[string]*configschema.Attribute{
   317  					"id":  {Type: cty.String, Optional: true, Computed: true},
   318  					"ami": {Type: cty.String, Optional: true},
   319  				},
   320  			},
   321  			RequiredReplace: cty.NewPathSet(cty.Path{
   322  				cty.GetAttrStep{Name: "ami"},
   323  			}),
   324  			Tainted: true,
   325  			ExpectedOutput: `  # test_instance.example is tainted, so must be replaced
   326  -/+ resource "test_instance" "example" {
   327        ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement
   328        ~ id  = "i-02ae66f368e8518a9" -> (known after apply)
   329      }
   330  `,
   331  		},
   332  		"force replacement with empty before value": {
   333  			Action: plans.DeleteThenCreate,
   334  			Mode:   addrs.ManagedResourceMode,
   335  			Before: cty.ObjectVal(map[string]cty.Value{
   336  				"name":   cty.StringVal("name"),
   337  				"forced": cty.NullVal(cty.String),
   338  			}),
   339  			After: cty.ObjectVal(map[string]cty.Value{
   340  				"name":   cty.StringVal("name"),
   341  				"forced": cty.StringVal("example"),
   342  			}),
   343  			Schema: &configschema.Block{
   344  				Attributes: map[string]*configschema.Attribute{
   345  					"name":   {Type: cty.String, Optional: true},
   346  					"forced": {Type: cty.String, Optional: true},
   347  				},
   348  			},
   349  			RequiredReplace: cty.NewPathSet(cty.Path{
   350  				cty.GetAttrStep{Name: "forced"},
   351  			}),
   352  			Tainted: false,
   353  			ExpectedOutput: `  # test_instance.example must be replaced
   354  -/+ resource "test_instance" "example" {
   355        + forced = "example" # forces replacement
   356          name   = "name"
   357      }
   358  `,
   359  		},
   360  		"force replacement with empty before value legacy": {
   361  			Action: plans.DeleteThenCreate,
   362  			Mode:   addrs.ManagedResourceMode,
   363  			Before: cty.ObjectVal(map[string]cty.Value{
   364  				"name":   cty.StringVal("name"),
   365  				"forced": cty.StringVal(""),
   366  			}),
   367  			After: cty.ObjectVal(map[string]cty.Value{
   368  				"name":   cty.StringVal("name"),
   369  				"forced": cty.StringVal("example"),
   370  			}),
   371  			Schema: &configschema.Block{
   372  				Attributes: map[string]*configschema.Attribute{
   373  					"name":   {Type: cty.String, Optional: true},
   374  					"forced": {Type: cty.String, Optional: true},
   375  				},
   376  			},
   377  			RequiredReplace: cty.NewPathSet(cty.Path{
   378  				cty.GetAttrStep{Name: "forced"},
   379  			}),
   380  			Tainted: false,
   381  			ExpectedOutput: `  # test_instance.example must be replaced
   382  -/+ resource "test_instance" "example" {
   383        + forced = "example" # forces replacement
   384          name   = "name"
   385      }
   386  `,
   387  		},
   388  	}
   389  
   390  	runTestCases(t, testCases)
   391  }
   392  
   393  func TestResourceChange_JSON(t *testing.T) {
   394  	testCases := map[string]testCase{
   395  		"creation": {
   396  			Action: plans.Create,
   397  			Mode:   addrs.ManagedResourceMode,
   398  			Before: cty.NullVal(cty.EmptyObject),
   399  			After: cty.ObjectVal(map[string]cty.Value{
   400  				"id": cty.UnknownVal(cty.String),
   401  				"json_field": cty.StringVal(`{
   402  					"str": "value",
   403  					"list":["a","b", 234, true],
   404  					"obj": {"key": "val"}
   405  				}`),
   406  			}),
   407  			Schema: &configschema.Block{
   408  				Attributes: map[string]*configschema.Attribute{
   409  					"id":         {Type: cty.String, Optional: true, Computed: true},
   410  					"json_field": {Type: cty.String, Optional: true},
   411  				},
   412  			},
   413  			RequiredReplace: cty.NewPathSet(),
   414  			Tainted:         false,
   415  			ExpectedOutput: `  # test_instance.example will be created
   416    + resource "test_instance" "example" {
   417        + id         = (known after apply)
   418        + json_field = jsonencode(
   419              {
   420                + list = [
   421                    + "a",
   422                    + "b",
   423                    + 234,
   424                    + true,
   425                  ]
   426                + obj  = {
   427                    + key = "val"
   428                  }
   429                + str  = "value"
   430              }
   431          )
   432      }
   433  `,
   434  		},
   435  		"in-place update of object": {
   436  			Action: plans.Update,
   437  			Mode:   addrs.ManagedResourceMode,
   438  			Before: cty.ObjectVal(map[string]cty.Value{
   439  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   440  				"json_field": cty.StringVal(`{"aaa": "value"}`),
   441  			}),
   442  			After: cty.ObjectVal(map[string]cty.Value{
   443  				"id":         cty.UnknownVal(cty.String),
   444  				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`),
   445  			}),
   446  			Schema: &configschema.Block{
   447  				Attributes: map[string]*configschema.Attribute{
   448  					"id":         {Type: cty.String, Optional: true, Computed: true},
   449  					"json_field": {Type: cty.String, Optional: true},
   450  				},
   451  			},
   452  			RequiredReplace: cty.NewPathSet(),
   453  			Tainted:         false,
   454  			ExpectedOutput: `  # test_instance.example will be updated in-place
   455    ~ resource "test_instance" "example" {
   456        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   457        ~ json_field = jsonencode(
   458            ~ {
   459                  aaa = "value"
   460                + bbb = "new_value"
   461              }
   462          )
   463      }
   464  `,
   465  		},
   466  		"in-place update (from empty tuple)": {
   467  			Action: plans.Update,
   468  			Mode:   addrs.ManagedResourceMode,
   469  			Before: cty.ObjectVal(map[string]cty.Value{
   470  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   471  				"json_field": cty.StringVal(`{"aaa": []}`),
   472  			}),
   473  			After: cty.ObjectVal(map[string]cty.Value{
   474  				"id":         cty.UnknownVal(cty.String),
   475  				"json_field": cty.StringVal(`{"aaa": ["value"]}`),
   476  			}),
   477  			Schema: &configschema.Block{
   478  				Attributes: map[string]*configschema.Attribute{
   479  					"id":         {Type: cty.String, Optional: true, Computed: true},
   480  					"json_field": {Type: cty.String, Optional: true},
   481  				},
   482  			},
   483  			RequiredReplace: cty.NewPathSet(),
   484  			Tainted:         false,
   485  			ExpectedOutput: `  # test_instance.example will be updated in-place
   486    ~ resource "test_instance" "example" {
   487        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   488        ~ json_field = jsonencode(
   489            ~ {
   490                ~ aaa = [
   491                    + "value",
   492                  ]
   493              }
   494          )
   495      }
   496  `,
   497  		},
   498  		"in-place update (to empty tuple)": {
   499  			Action: plans.Update,
   500  			Mode:   addrs.ManagedResourceMode,
   501  			Before: cty.ObjectVal(map[string]cty.Value{
   502  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   503  				"json_field": cty.StringVal(`{"aaa": ["value"]}`),
   504  			}),
   505  			After: cty.ObjectVal(map[string]cty.Value{
   506  				"id":         cty.UnknownVal(cty.String),
   507  				"json_field": cty.StringVal(`{"aaa": []}`),
   508  			}),
   509  			Schema: &configschema.Block{
   510  				Attributes: map[string]*configschema.Attribute{
   511  					"id":         {Type: cty.String, Optional: true, Computed: true},
   512  					"json_field": {Type: cty.String, Optional: true},
   513  				},
   514  			},
   515  			RequiredReplace: cty.NewPathSet(),
   516  			Tainted:         false,
   517  			ExpectedOutput: `  # test_instance.example will be updated in-place
   518    ~ resource "test_instance" "example" {
   519        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   520        ~ json_field = jsonencode(
   521            ~ {
   522                ~ aaa = [
   523                    - "value",
   524                  ]
   525              }
   526          )
   527      }
   528  `,
   529  		},
   530  		"in-place update (tuple of different types)": {
   531  			Action: plans.Update,
   532  			Mode:   addrs.ManagedResourceMode,
   533  			Before: cty.ObjectVal(map[string]cty.Value{
   534  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   535  				"json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`),
   536  			}),
   537  			After: cty.ObjectVal(map[string]cty.Value{
   538  				"id":         cty.UnknownVal(cty.String),
   539  				"json_field": cty.StringVal(`{"aaa": [42, {"foo":"baz"}, "value"]}`),
   540  			}),
   541  			Schema: &configschema.Block{
   542  				Attributes: map[string]*configschema.Attribute{
   543  					"id":         {Type: cty.String, Optional: true, Computed: true},
   544  					"json_field": {Type: cty.String, Optional: true},
   545  				},
   546  			},
   547  			RequiredReplace: cty.NewPathSet(),
   548  			Tainted:         false,
   549  			ExpectedOutput: `  # test_instance.example will be updated in-place
   550    ~ resource "test_instance" "example" {
   551        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   552        ~ json_field = jsonencode(
   553            ~ {
   554                ~ aaa = [
   555                      42,
   556                    ~ {
   557                        ~ foo = "bar" -> "baz"
   558                      },
   559                      "value",
   560                  ]
   561              }
   562          )
   563      }
   564  `,
   565  		},
   566  		"force-new update": {
   567  			Action: plans.DeleteThenCreate,
   568  			Mode:   addrs.ManagedResourceMode,
   569  			Before: cty.ObjectVal(map[string]cty.Value{
   570  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   571  				"json_field": cty.StringVal(`{"aaa": "value"}`),
   572  			}),
   573  			After: cty.ObjectVal(map[string]cty.Value{
   574  				"id":         cty.UnknownVal(cty.String),
   575  				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`),
   576  			}),
   577  			Schema: &configschema.Block{
   578  				Attributes: map[string]*configschema.Attribute{
   579  					"id":         {Type: cty.String, Optional: true, Computed: true},
   580  					"json_field": {Type: cty.String, Optional: true},
   581  				},
   582  			},
   583  			RequiredReplace: cty.NewPathSet(cty.Path{
   584  				cty.GetAttrStep{Name: "json_field"},
   585  			}),
   586  			Tainted: false,
   587  			ExpectedOutput: `  # test_instance.example must be replaced
   588  -/+ resource "test_instance" "example" {
   589        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   590        ~ json_field = jsonencode(
   591            ~ {
   592                  aaa = "value"
   593                + bbb = "new_value"
   594              } # forces replacement
   595          )
   596      }
   597  `,
   598  		},
   599  		"in-place update (whitespace change)": {
   600  			Action: plans.Update,
   601  			Mode:   addrs.ManagedResourceMode,
   602  			Before: cty.ObjectVal(map[string]cty.Value{
   603  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   604  				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`),
   605  			}),
   606  			After: cty.ObjectVal(map[string]cty.Value{
   607  				"id": cty.UnknownVal(cty.String),
   608  				"json_field": cty.StringVal(`{"aaa":"value",
   609  					"bbb":"another"}`),
   610  			}),
   611  			Schema: &configschema.Block{
   612  				Attributes: map[string]*configschema.Attribute{
   613  					"id":         {Type: cty.String, Optional: true, Computed: true},
   614  					"json_field": {Type: cty.String, Optional: true},
   615  				},
   616  			},
   617  			RequiredReplace: cty.NewPathSet(),
   618  			Tainted:         false,
   619  			ExpectedOutput: `  # test_instance.example will be updated in-place
   620    ~ resource "test_instance" "example" {
   621        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   622        ~ json_field = jsonencode( # whitespace changes
   623              {
   624                  aaa = "value"
   625                  bbb = "another"
   626              }
   627          )
   628      }
   629  `,
   630  		},
   631  		"force-new update (whitespace change)": {
   632  			Action: plans.DeleteThenCreate,
   633  			Mode:   addrs.ManagedResourceMode,
   634  			Before: cty.ObjectVal(map[string]cty.Value{
   635  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   636  				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`),
   637  			}),
   638  			After: cty.ObjectVal(map[string]cty.Value{
   639  				"id": cty.UnknownVal(cty.String),
   640  				"json_field": cty.StringVal(`{"aaa":"value",
   641  					"bbb":"another"}`),
   642  			}),
   643  			Schema: &configschema.Block{
   644  				Attributes: map[string]*configschema.Attribute{
   645  					"id":         {Type: cty.String, Optional: true, Computed: true},
   646  					"json_field": {Type: cty.String, Optional: true},
   647  				},
   648  			},
   649  			RequiredReplace: cty.NewPathSet(cty.Path{
   650  				cty.GetAttrStep{Name: "json_field"},
   651  			}),
   652  			Tainted: false,
   653  			ExpectedOutput: `  # test_instance.example must be replaced
   654  -/+ resource "test_instance" "example" {
   655        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   656        ~ json_field = jsonencode( # whitespace changes force replacement
   657              {
   658                  aaa = "value"
   659                  bbb = "another"
   660              }
   661          )
   662      }
   663  `,
   664  		},
   665  		"creation (empty)": {
   666  			Action: plans.Create,
   667  			Mode:   addrs.ManagedResourceMode,
   668  			Before: cty.NullVal(cty.EmptyObject),
   669  			After: cty.ObjectVal(map[string]cty.Value{
   670  				"id":         cty.UnknownVal(cty.String),
   671  				"json_field": cty.StringVal(`{}`),
   672  			}),
   673  			Schema: &configschema.Block{
   674  				Attributes: map[string]*configschema.Attribute{
   675  					"id":         {Type: cty.String, Optional: true, Computed: true},
   676  					"json_field": {Type: cty.String, Optional: true},
   677  				},
   678  			},
   679  			RequiredReplace: cty.NewPathSet(),
   680  			Tainted:         false,
   681  			ExpectedOutput: `  # test_instance.example will be created
   682    + resource "test_instance" "example" {
   683        + id         = (known after apply)
   684        + json_field = jsonencode({})
   685      }
   686  `,
   687  		},
   688  		"JSON list item removal": {
   689  			Action: plans.Update,
   690  			Mode:   addrs.ManagedResourceMode,
   691  			Before: cty.ObjectVal(map[string]cty.Value{
   692  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   693  				"json_field": cty.StringVal(`["first","second","third"]`),
   694  			}),
   695  			After: cty.ObjectVal(map[string]cty.Value{
   696  				"id":         cty.UnknownVal(cty.String),
   697  				"json_field": cty.StringVal(`["first","second"]`),
   698  			}),
   699  			Schema: &configschema.Block{
   700  				Attributes: map[string]*configschema.Attribute{
   701  					"id":         {Type: cty.String, Optional: true, Computed: true},
   702  					"json_field": {Type: cty.String, Optional: true},
   703  				},
   704  			},
   705  			RequiredReplace: cty.NewPathSet(),
   706  			Tainted:         false,
   707  			ExpectedOutput: `  # test_instance.example will be updated in-place
   708    ~ resource "test_instance" "example" {
   709        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   710        ~ json_field = jsonencode(
   711            ~ [
   712                  "first",
   713                  "second",
   714                - "third",
   715              ]
   716          )
   717      }
   718  `,
   719  		},
   720  		"JSON list item addition": {
   721  			Action: plans.Update,
   722  			Mode:   addrs.ManagedResourceMode,
   723  			Before: cty.ObjectVal(map[string]cty.Value{
   724  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   725  				"json_field": cty.StringVal(`["first","second"]`),
   726  			}),
   727  			After: cty.ObjectVal(map[string]cty.Value{
   728  				"id":         cty.UnknownVal(cty.String),
   729  				"json_field": cty.StringVal(`["first","second","third"]`),
   730  			}),
   731  			Schema: &configschema.Block{
   732  				Attributes: map[string]*configschema.Attribute{
   733  					"id":         {Type: cty.String, Optional: true, Computed: true},
   734  					"json_field": {Type: cty.String, Optional: true},
   735  				},
   736  			},
   737  			RequiredReplace: cty.NewPathSet(),
   738  			Tainted:         false,
   739  			ExpectedOutput: `  # test_instance.example will be updated in-place
   740    ~ resource "test_instance" "example" {
   741        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   742        ~ json_field = jsonencode(
   743            ~ [
   744                  "first",
   745                  "second",
   746                + "third",
   747              ]
   748          )
   749      }
   750  `,
   751  		},
   752  		"JSON list object addition": {
   753  			Action: plans.Update,
   754  			Mode:   addrs.ManagedResourceMode,
   755  			Before: cty.ObjectVal(map[string]cty.Value{
   756  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   757  				"json_field": cty.StringVal(`{"first":"111"}`),
   758  			}),
   759  			After: cty.ObjectVal(map[string]cty.Value{
   760  				"id":         cty.UnknownVal(cty.String),
   761  				"json_field": cty.StringVal(`{"first":"111","second":"222"}`),
   762  			}),
   763  			Schema: &configschema.Block{
   764  				Attributes: map[string]*configschema.Attribute{
   765  					"id":         {Type: cty.String, Optional: true, Computed: true},
   766  					"json_field": {Type: cty.String, Optional: true},
   767  				},
   768  			},
   769  			RequiredReplace: cty.NewPathSet(),
   770  			Tainted:         false,
   771  			ExpectedOutput: `  # test_instance.example will be updated in-place
   772    ~ resource "test_instance" "example" {
   773        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   774        ~ json_field = jsonencode(
   775            ~ {
   776                  first  = "111"
   777                + second = "222"
   778              }
   779          )
   780      }
   781  `,
   782  		},
   783  		"JSON object with nested list": {
   784  			Action: plans.Update,
   785  			Mode:   addrs.ManagedResourceMode,
   786  			Before: cty.ObjectVal(map[string]cty.Value{
   787  				"id": cty.StringVal("i-02ae66f368e8518a9"),
   788  				"json_field": cty.StringVal(`{
   789  		  "Statement": ["first"]
   790  		}`),
   791  			}),
   792  			After: cty.ObjectVal(map[string]cty.Value{
   793  				"id": cty.UnknownVal(cty.String),
   794  				"json_field": cty.StringVal(`{
   795  		  "Statement": ["first", "second"]
   796  		}`),
   797  			}),
   798  			Schema: &configschema.Block{
   799  				Attributes: map[string]*configschema.Attribute{
   800  					"id":         {Type: cty.String, Optional: true, Computed: true},
   801  					"json_field": {Type: cty.String, Optional: true},
   802  				},
   803  			},
   804  			RequiredReplace: cty.NewPathSet(),
   805  			Tainted:         false,
   806  			ExpectedOutput: `  # test_instance.example will be updated in-place
   807    ~ resource "test_instance" "example" {
   808        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   809        ~ json_field = jsonencode(
   810            ~ {
   811                ~ Statement = [
   812                      "first",
   813                    + "second",
   814                  ]
   815              }
   816          )
   817      }
   818  `,
   819  		},
   820  		"JSON list of objects - adding item": {
   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(`[{"one": "111"}]`),
   826  			}),
   827  			After: cty.ObjectVal(map[string]cty.Value{
   828  				"id":         cty.UnknownVal(cty.String),
   829  				"json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}]`),
   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  			Tainted:         false,
   839  			ExpectedOutput: `  # test_instance.example will be updated in-place
   840    ~ resource "test_instance" "example" {
   841        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   842        ~ json_field = jsonencode(
   843            ~ [
   844                  {
   845                      one = "111"
   846                  },
   847                + {
   848                    + two = "222"
   849                  },
   850              ]
   851          )
   852      }
   853  `,
   854  		},
   855  		"JSON list of objects - removing item": {
   856  			Action: plans.Update,
   857  			Mode:   addrs.ManagedResourceMode,
   858  			Before: cty.ObjectVal(map[string]cty.Value{
   859  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   860  				"json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}]`),
   861  			}),
   862  			After: cty.ObjectVal(map[string]cty.Value{
   863  				"id":         cty.UnknownVal(cty.String),
   864  				"json_field": cty.StringVal(`[{"one": "111"}]`),
   865  			}),
   866  			Schema: &configschema.Block{
   867  				Attributes: map[string]*configschema.Attribute{
   868  					"id":         {Type: cty.String, Optional: true, Computed: true},
   869  					"json_field": {Type: cty.String, Optional: true},
   870  				},
   871  			},
   872  			RequiredReplace: cty.NewPathSet(),
   873  			Tainted:         false,
   874  			ExpectedOutput: `  # test_instance.example will be updated in-place
   875    ~ resource "test_instance" "example" {
   876        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   877        ~ json_field = jsonencode(
   878            ~ [
   879                  {
   880                      one = "111"
   881                  },
   882                - {
   883                    - two = "222"
   884                  },
   885              ]
   886          )
   887      }
   888  `,
   889  		},
   890  		"JSON object with list of objects": {
   891  			Action: plans.Update,
   892  			Mode:   addrs.ManagedResourceMode,
   893  			Before: cty.ObjectVal(map[string]cty.Value{
   894  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   895  				"json_field": cty.StringVal(`{"parent":[{"one": "111"}]}`),
   896  			}),
   897  			After: cty.ObjectVal(map[string]cty.Value{
   898  				"id":         cty.UnknownVal(cty.String),
   899  				"json_field": cty.StringVal(`{"parent":[{"one": "111"}, {"two": "222"}]}`),
   900  			}),
   901  			Schema: &configschema.Block{
   902  				Attributes: map[string]*configschema.Attribute{
   903  					"id":         {Type: cty.String, Optional: true, Computed: true},
   904  					"json_field": {Type: cty.String, Optional: true},
   905  				},
   906  			},
   907  			RequiredReplace: cty.NewPathSet(),
   908  			Tainted:         false,
   909  			ExpectedOutput: `  # test_instance.example will be updated in-place
   910    ~ resource "test_instance" "example" {
   911        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   912        ~ json_field = jsonencode(
   913            ~ {
   914                ~ parent = [
   915                      {
   916                          one = "111"
   917                      },
   918                    + {
   919                        + two = "222"
   920                      },
   921                  ]
   922              }
   923          )
   924      }
   925  `,
   926  		},
   927  		"JSON object double nested lists": {
   928  			Action: plans.Update,
   929  			Mode:   addrs.ManagedResourceMode,
   930  			Before: cty.ObjectVal(map[string]cty.Value{
   931  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   932  				"json_field": cty.StringVal(`{"parent":[{"another_list": ["111"]}]}`),
   933  			}),
   934  			After: cty.ObjectVal(map[string]cty.Value{
   935  				"id":         cty.UnknownVal(cty.String),
   936  				"json_field": cty.StringVal(`{"parent":[{"another_list": ["111", "222"]}]}`),
   937  			}),
   938  			Schema: &configschema.Block{
   939  				Attributes: map[string]*configschema.Attribute{
   940  					"id":         {Type: cty.String, Optional: true, Computed: true},
   941  					"json_field": {Type: cty.String, Optional: true},
   942  				},
   943  			},
   944  			RequiredReplace: cty.NewPathSet(),
   945  			Tainted:         false,
   946  			ExpectedOutput: `  # test_instance.example will be updated in-place
   947    ~ resource "test_instance" "example" {
   948        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   949        ~ json_field = jsonencode(
   950            ~ {
   951                ~ parent = [
   952                    ~ {
   953                        ~ another_list = [
   954                              "111",
   955                            + "222",
   956                          ]
   957                      },
   958                  ]
   959              }
   960          )
   961      }
   962  `,
   963  		},
   964  		"in-place update from object to tuple": {
   965  			Action: plans.Update,
   966  			Mode:   addrs.ManagedResourceMode,
   967  			Before: cty.ObjectVal(map[string]cty.Value{
   968  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
   969  				"json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`),
   970  			}),
   971  			After: cty.ObjectVal(map[string]cty.Value{
   972  				"id":         cty.UnknownVal(cty.String),
   973  				"json_field": cty.StringVal(`["aaa", 42, "something"]`),
   974  			}),
   975  			Schema: &configschema.Block{
   976  				Attributes: map[string]*configschema.Attribute{
   977  					"id":         {Type: cty.String, Optional: true, Computed: true},
   978  					"json_field": {Type: cty.String, Optional: true},
   979  				},
   980  			},
   981  			RequiredReplace: cty.NewPathSet(),
   982  			Tainted:         false,
   983  			ExpectedOutput: `  # test_instance.example will be updated in-place
   984    ~ resource "test_instance" "example" {
   985        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
   986        ~ json_field = jsonencode(
   987            ~ {
   988                - aaa = [
   989                    - 42,
   990                    - {
   991                        - foo = "bar"
   992                      },
   993                    - "value",
   994                  ]
   995              } -> [
   996                + "aaa",
   997                + 42,
   998                + "something",
   999              ]
  1000          )
  1001      }
  1002  `,
  1003  		},
  1004  	}
  1005  	runTestCases(t, testCases)
  1006  }
  1007  
  1008  func TestResourceChange_primitiveList(t *testing.T) {
  1009  	testCases := map[string]testCase{
  1010  		"in-place update - creation": {
  1011  			Action: plans.Update,
  1012  			Mode:   addrs.ManagedResourceMode,
  1013  			Before: cty.ObjectVal(map[string]cty.Value{
  1014  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1015  				"ami":        cty.StringVal("ami-STATIC"),
  1016  				"list_field": cty.NullVal(cty.List(cty.String)),
  1017  			}),
  1018  			After: cty.ObjectVal(map[string]cty.Value{
  1019  				"id":  cty.UnknownVal(cty.String),
  1020  				"ami": cty.StringVal("ami-STATIC"),
  1021  				"list_field": cty.ListVal([]cty.Value{
  1022  					cty.StringVal("new-element"),
  1023  				}),
  1024  			}),
  1025  			Schema: &configschema.Block{
  1026  				Attributes: map[string]*configschema.Attribute{
  1027  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1028  					"ami":        {Type: cty.String, Optional: true},
  1029  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1030  				},
  1031  			},
  1032  			RequiredReplace: cty.NewPathSet(),
  1033  			Tainted:         false,
  1034  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1035    ~ resource "test_instance" "example" {
  1036          ami        = "ami-STATIC"
  1037        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1038        + list_field = [
  1039            + "new-element",
  1040          ]
  1041      }
  1042  `,
  1043  		},
  1044  		"in-place update - first addition": {
  1045  			Action: plans.Update,
  1046  			Mode:   addrs.ManagedResourceMode,
  1047  			Before: cty.ObjectVal(map[string]cty.Value{
  1048  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1049  				"ami":        cty.StringVal("ami-STATIC"),
  1050  				"list_field": cty.ListValEmpty(cty.String),
  1051  			}),
  1052  			After: cty.ObjectVal(map[string]cty.Value{
  1053  				"id":  cty.UnknownVal(cty.String),
  1054  				"ami": cty.StringVal("ami-STATIC"),
  1055  				"list_field": cty.ListVal([]cty.Value{
  1056  					cty.StringVal("new-element"),
  1057  				}),
  1058  			}),
  1059  			Schema: &configschema.Block{
  1060  				Attributes: map[string]*configschema.Attribute{
  1061  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1062  					"ami":        {Type: cty.String, Optional: true},
  1063  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1064  				},
  1065  			},
  1066  			RequiredReplace: cty.NewPathSet(),
  1067  			Tainted:         false,
  1068  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1069    ~ resource "test_instance" "example" {
  1070          ami        = "ami-STATIC"
  1071        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1072        ~ list_field = [
  1073            + "new-element",
  1074          ]
  1075      }
  1076  `,
  1077  		},
  1078  		"in-place update - insertion": {
  1079  			Action: plans.Update,
  1080  			Mode:   addrs.ManagedResourceMode,
  1081  			Before: cty.ObjectVal(map[string]cty.Value{
  1082  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1083  				"ami": cty.StringVal("ami-STATIC"),
  1084  				"list_field": cty.ListVal([]cty.Value{
  1085  					cty.StringVal("aaaa"),
  1086  					cty.StringVal("cccc"),
  1087  				}),
  1088  			}),
  1089  			After: cty.ObjectVal(map[string]cty.Value{
  1090  				"id":  cty.UnknownVal(cty.String),
  1091  				"ami": cty.StringVal("ami-STATIC"),
  1092  				"list_field": cty.ListVal([]cty.Value{
  1093  					cty.StringVal("aaaa"),
  1094  					cty.StringVal("bbbb"),
  1095  					cty.StringVal("cccc"),
  1096  				}),
  1097  			}),
  1098  			Schema: &configschema.Block{
  1099  				Attributes: map[string]*configschema.Attribute{
  1100  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1101  					"ami":        {Type: cty.String, Optional: true},
  1102  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1103  				},
  1104  			},
  1105  			RequiredReplace: cty.NewPathSet(),
  1106  			Tainted:         false,
  1107  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1108    ~ resource "test_instance" "example" {
  1109          ami        = "ami-STATIC"
  1110        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1111        ~ list_field = [
  1112              "aaaa",
  1113            + "bbbb",
  1114              "cccc",
  1115          ]
  1116      }
  1117  `,
  1118  		},
  1119  		"force-new update - insertion": {
  1120  			Action: plans.DeleteThenCreate,
  1121  			Mode:   addrs.ManagedResourceMode,
  1122  			Before: cty.ObjectVal(map[string]cty.Value{
  1123  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1124  				"ami": cty.StringVal("ami-STATIC"),
  1125  				"list_field": cty.ListVal([]cty.Value{
  1126  					cty.StringVal("aaaa"),
  1127  					cty.StringVal("cccc"),
  1128  				}),
  1129  			}),
  1130  			After: cty.ObjectVal(map[string]cty.Value{
  1131  				"id":  cty.UnknownVal(cty.String),
  1132  				"ami": cty.StringVal("ami-STATIC"),
  1133  				"list_field": cty.ListVal([]cty.Value{
  1134  					cty.StringVal("aaaa"),
  1135  					cty.StringVal("bbbb"),
  1136  					cty.StringVal("cccc"),
  1137  				}),
  1138  			}),
  1139  			Schema: &configschema.Block{
  1140  				Attributes: map[string]*configschema.Attribute{
  1141  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1142  					"ami":        {Type: cty.String, Optional: true},
  1143  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1144  				},
  1145  			},
  1146  			RequiredReplace: cty.NewPathSet(cty.Path{
  1147  				cty.GetAttrStep{Name: "list_field"},
  1148  			}),
  1149  			Tainted: false,
  1150  			ExpectedOutput: `  # test_instance.example must be replaced
  1151  -/+ resource "test_instance" "example" {
  1152          ami        = "ami-STATIC"
  1153        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1154        ~ list_field = [ # forces replacement
  1155              "aaaa",
  1156            + "bbbb",
  1157              "cccc",
  1158          ]
  1159      }
  1160  `,
  1161  		},
  1162  		"in-place update - deletion": {
  1163  			Action: plans.Update,
  1164  			Mode:   addrs.ManagedResourceMode,
  1165  			Before: cty.ObjectVal(map[string]cty.Value{
  1166  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1167  				"ami": cty.StringVal("ami-STATIC"),
  1168  				"list_field": cty.ListVal([]cty.Value{
  1169  					cty.StringVal("aaaa"),
  1170  					cty.StringVal("bbbb"),
  1171  					cty.StringVal("cccc"),
  1172  				}),
  1173  			}),
  1174  			After: cty.ObjectVal(map[string]cty.Value{
  1175  				"id":  cty.UnknownVal(cty.String),
  1176  				"ami": cty.StringVal("ami-STATIC"),
  1177  				"list_field": cty.ListVal([]cty.Value{
  1178  					cty.StringVal("bbbb"),
  1179  				}),
  1180  			}),
  1181  			Schema: &configschema.Block{
  1182  				Attributes: map[string]*configschema.Attribute{
  1183  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1184  					"ami":        {Type: cty.String, Optional: true},
  1185  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1186  				},
  1187  			},
  1188  			RequiredReplace: cty.NewPathSet(),
  1189  			Tainted:         false,
  1190  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1191    ~ resource "test_instance" "example" {
  1192          ami        = "ami-STATIC"
  1193        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1194        ~ list_field = [
  1195            - "aaaa",
  1196              "bbbb",
  1197            - "cccc",
  1198          ]
  1199      }
  1200  `,
  1201  		},
  1202  		"creation - empty list": {
  1203  			Action: plans.Create,
  1204  			Mode:   addrs.ManagedResourceMode,
  1205  			Before: cty.NullVal(cty.EmptyObject),
  1206  			After: cty.ObjectVal(map[string]cty.Value{
  1207  				"id":         cty.UnknownVal(cty.String),
  1208  				"ami":        cty.StringVal("ami-STATIC"),
  1209  				"list_field": cty.ListValEmpty(cty.String),
  1210  			}),
  1211  			Schema: &configschema.Block{
  1212  				Attributes: map[string]*configschema.Attribute{
  1213  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1214  					"ami":        {Type: cty.String, Optional: true},
  1215  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1216  				},
  1217  			},
  1218  			RequiredReplace: cty.NewPathSet(),
  1219  			Tainted:         false,
  1220  			ExpectedOutput: `  # test_instance.example will be created
  1221    + resource "test_instance" "example" {
  1222        + ami        = "ami-STATIC"
  1223        + id         = (known after apply)
  1224        + list_field = []
  1225      }
  1226  `,
  1227  		},
  1228  		"in-place update - full to empty": {
  1229  			Action: plans.Update,
  1230  			Mode:   addrs.ManagedResourceMode,
  1231  			Before: cty.ObjectVal(map[string]cty.Value{
  1232  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1233  				"ami": cty.StringVal("ami-STATIC"),
  1234  				"list_field": cty.ListVal([]cty.Value{
  1235  					cty.StringVal("aaaa"),
  1236  					cty.StringVal("bbbb"),
  1237  					cty.StringVal("cccc"),
  1238  				}),
  1239  			}),
  1240  			After: cty.ObjectVal(map[string]cty.Value{
  1241  				"id":         cty.UnknownVal(cty.String),
  1242  				"ami":        cty.StringVal("ami-STATIC"),
  1243  				"list_field": cty.ListValEmpty(cty.String),
  1244  			}),
  1245  			Schema: &configschema.Block{
  1246  				Attributes: map[string]*configschema.Attribute{
  1247  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1248  					"ami":        {Type: cty.String, Optional: true},
  1249  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1250  				},
  1251  			},
  1252  			RequiredReplace: cty.NewPathSet(),
  1253  			Tainted:         false,
  1254  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1255    ~ resource "test_instance" "example" {
  1256          ami        = "ami-STATIC"
  1257        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1258        ~ list_field = [
  1259            - "aaaa",
  1260            - "bbbb",
  1261            - "cccc",
  1262          ]
  1263      }
  1264  `,
  1265  		},
  1266  		"in-place update - null to empty": {
  1267  			Action: plans.Update,
  1268  			Mode:   addrs.ManagedResourceMode,
  1269  			Before: cty.ObjectVal(map[string]cty.Value{
  1270  				"id":         cty.StringVal("i-02ae66f368e8518a9"),
  1271  				"ami":        cty.StringVal("ami-STATIC"),
  1272  				"list_field": cty.NullVal(cty.List(cty.String)),
  1273  			}),
  1274  			After: cty.ObjectVal(map[string]cty.Value{
  1275  				"id":         cty.UnknownVal(cty.String),
  1276  				"ami":        cty.StringVal("ami-STATIC"),
  1277  				"list_field": cty.ListValEmpty(cty.String),
  1278  			}),
  1279  			Schema: &configschema.Block{
  1280  				Attributes: map[string]*configschema.Attribute{
  1281  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1282  					"ami":        {Type: cty.String, Optional: true},
  1283  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1284  				},
  1285  			},
  1286  			RequiredReplace: cty.NewPathSet(),
  1287  			Tainted:         false,
  1288  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1289    ~ resource "test_instance" "example" {
  1290          ami        = "ami-STATIC"
  1291        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1292        + list_field = []
  1293      }
  1294  `,
  1295  		},
  1296  		"update to unknown element": {
  1297  			Action: plans.Update,
  1298  			Mode:   addrs.ManagedResourceMode,
  1299  			Before: cty.ObjectVal(map[string]cty.Value{
  1300  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1301  				"ami": cty.StringVal("ami-STATIC"),
  1302  				"list_field": cty.ListVal([]cty.Value{
  1303  					cty.StringVal("aaaa"),
  1304  					cty.StringVal("bbbb"),
  1305  					cty.StringVal("cccc"),
  1306  				}),
  1307  			}),
  1308  			After: cty.ObjectVal(map[string]cty.Value{
  1309  				"id":  cty.UnknownVal(cty.String),
  1310  				"ami": cty.StringVal("ami-STATIC"),
  1311  				"list_field": cty.ListVal([]cty.Value{
  1312  					cty.StringVal("aaaa"),
  1313  					cty.UnknownVal(cty.String),
  1314  					cty.StringVal("cccc"),
  1315  				}),
  1316  			}),
  1317  			Schema: &configschema.Block{
  1318  				Attributes: map[string]*configschema.Attribute{
  1319  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1320  					"ami":        {Type: cty.String, Optional: true},
  1321  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1322  				},
  1323  			},
  1324  			RequiredReplace: cty.NewPathSet(),
  1325  			Tainted:         false,
  1326  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1327    ~ resource "test_instance" "example" {
  1328          ami        = "ami-STATIC"
  1329        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1330        ~ list_field = [
  1331              "aaaa",
  1332            - "bbbb",
  1333            + (known after apply),
  1334              "cccc",
  1335          ]
  1336      }
  1337  `,
  1338  		},
  1339  		"update - two new unknown elements": {
  1340  			Action: plans.Update,
  1341  			Mode:   addrs.ManagedResourceMode,
  1342  			Before: cty.ObjectVal(map[string]cty.Value{
  1343  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1344  				"ami": cty.StringVal("ami-STATIC"),
  1345  				"list_field": cty.ListVal([]cty.Value{
  1346  					cty.StringVal("aaaa"),
  1347  					cty.StringVal("bbbb"),
  1348  					cty.StringVal("cccc"),
  1349  				}),
  1350  			}),
  1351  			After: cty.ObjectVal(map[string]cty.Value{
  1352  				"id":  cty.UnknownVal(cty.String),
  1353  				"ami": cty.StringVal("ami-STATIC"),
  1354  				"list_field": cty.ListVal([]cty.Value{
  1355  					cty.StringVal("aaaa"),
  1356  					cty.UnknownVal(cty.String),
  1357  					cty.UnknownVal(cty.String),
  1358  					cty.StringVal("cccc"),
  1359  				}),
  1360  			}),
  1361  			Schema: &configschema.Block{
  1362  				Attributes: map[string]*configschema.Attribute{
  1363  					"id":         {Type: cty.String, Optional: true, Computed: true},
  1364  					"ami":        {Type: cty.String, Optional: true},
  1365  					"list_field": {Type: cty.List(cty.String), Optional: true},
  1366  				},
  1367  			},
  1368  			RequiredReplace: cty.NewPathSet(),
  1369  			Tainted:         false,
  1370  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1371    ~ resource "test_instance" "example" {
  1372          ami        = "ami-STATIC"
  1373        ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
  1374        ~ list_field = [
  1375              "aaaa",
  1376            - "bbbb",
  1377            + (known after apply),
  1378            + (known after apply),
  1379              "cccc",
  1380          ]
  1381      }
  1382  `,
  1383  		},
  1384  	}
  1385  	runTestCases(t, testCases)
  1386  }
  1387  
  1388  func TestResourceChange_primitiveSet(t *testing.T) {
  1389  	testCases := map[string]testCase{
  1390  		"in-place update - creation": {
  1391  			Action: plans.Update,
  1392  			Mode:   addrs.ManagedResourceMode,
  1393  			Before: cty.ObjectVal(map[string]cty.Value{
  1394  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
  1395  				"ami":       cty.StringVal("ami-STATIC"),
  1396  				"set_field": cty.NullVal(cty.Set(cty.String)),
  1397  			}),
  1398  			After: cty.ObjectVal(map[string]cty.Value{
  1399  				"id":  cty.UnknownVal(cty.String),
  1400  				"ami": cty.StringVal("ami-STATIC"),
  1401  				"set_field": cty.SetVal([]cty.Value{
  1402  					cty.StringVal("new-element"),
  1403  				}),
  1404  			}),
  1405  			Schema: &configschema.Block{
  1406  				Attributes: map[string]*configschema.Attribute{
  1407  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1408  					"ami":       {Type: cty.String, Optional: true},
  1409  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  1410  				},
  1411  			},
  1412  			RequiredReplace: cty.NewPathSet(),
  1413  			Tainted:         false,
  1414  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1415    ~ resource "test_instance" "example" {
  1416          ami       = "ami-STATIC"
  1417        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1418        + set_field = [
  1419            + "new-element",
  1420          ]
  1421      }
  1422  `,
  1423  		},
  1424  		"in-place update - first insertion": {
  1425  			Action: plans.Update,
  1426  			Mode:   addrs.ManagedResourceMode,
  1427  			Before: cty.ObjectVal(map[string]cty.Value{
  1428  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
  1429  				"ami":       cty.StringVal("ami-STATIC"),
  1430  				"set_field": cty.SetValEmpty(cty.String),
  1431  			}),
  1432  			After: cty.ObjectVal(map[string]cty.Value{
  1433  				"id":  cty.UnknownVal(cty.String),
  1434  				"ami": cty.StringVal("ami-STATIC"),
  1435  				"set_field": cty.SetVal([]cty.Value{
  1436  					cty.StringVal("new-element"),
  1437  				}),
  1438  			}),
  1439  			Schema: &configschema.Block{
  1440  				Attributes: map[string]*configschema.Attribute{
  1441  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1442  					"ami":       {Type: cty.String, Optional: true},
  1443  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  1444  				},
  1445  			},
  1446  			RequiredReplace: cty.NewPathSet(),
  1447  			Tainted:         false,
  1448  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1449    ~ resource "test_instance" "example" {
  1450          ami       = "ami-STATIC"
  1451        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1452        ~ set_field = [
  1453            + "new-element",
  1454          ]
  1455      }
  1456  `,
  1457  		},
  1458  		"in-place update - insertion": {
  1459  			Action: plans.Update,
  1460  			Mode:   addrs.ManagedResourceMode,
  1461  			Before: cty.ObjectVal(map[string]cty.Value{
  1462  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1463  				"ami": cty.StringVal("ami-STATIC"),
  1464  				"set_field": cty.SetVal([]cty.Value{
  1465  					cty.StringVal("aaaa"),
  1466  					cty.StringVal("cccc"),
  1467  				}),
  1468  			}),
  1469  			After: cty.ObjectVal(map[string]cty.Value{
  1470  				"id":  cty.UnknownVal(cty.String),
  1471  				"ami": cty.StringVal("ami-STATIC"),
  1472  				"set_field": cty.SetVal([]cty.Value{
  1473  					cty.StringVal("aaaa"),
  1474  					cty.StringVal("bbbb"),
  1475  					cty.StringVal("cccc"),
  1476  				}),
  1477  			}),
  1478  			Schema: &configschema.Block{
  1479  				Attributes: map[string]*configschema.Attribute{
  1480  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1481  					"ami":       {Type: cty.String, Optional: true},
  1482  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  1483  				},
  1484  			},
  1485  			RequiredReplace: cty.NewPathSet(),
  1486  			Tainted:         false,
  1487  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1488    ~ resource "test_instance" "example" {
  1489          ami       = "ami-STATIC"
  1490        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1491        ~ set_field = [
  1492              "aaaa",
  1493            + "bbbb",
  1494              "cccc",
  1495          ]
  1496      }
  1497  `,
  1498  		},
  1499  		"force-new update - insertion": {
  1500  			Action: plans.DeleteThenCreate,
  1501  			Mode:   addrs.ManagedResourceMode,
  1502  			Before: cty.ObjectVal(map[string]cty.Value{
  1503  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1504  				"ami": cty.StringVal("ami-STATIC"),
  1505  				"set_field": cty.SetVal([]cty.Value{
  1506  					cty.StringVal("aaaa"),
  1507  					cty.StringVal("cccc"),
  1508  				}),
  1509  			}),
  1510  			After: cty.ObjectVal(map[string]cty.Value{
  1511  				"id":  cty.UnknownVal(cty.String),
  1512  				"ami": cty.StringVal("ami-STATIC"),
  1513  				"set_field": cty.SetVal([]cty.Value{
  1514  					cty.StringVal("aaaa"),
  1515  					cty.StringVal("bbbb"),
  1516  					cty.StringVal("cccc"),
  1517  				}),
  1518  			}),
  1519  			Schema: &configschema.Block{
  1520  				Attributes: map[string]*configschema.Attribute{
  1521  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1522  					"ami":       {Type: cty.String, Optional: true},
  1523  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  1524  				},
  1525  			},
  1526  			RequiredReplace: cty.NewPathSet(cty.Path{
  1527  				cty.GetAttrStep{Name: "set_field"},
  1528  			}),
  1529  			Tainted: false,
  1530  			ExpectedOutput: `  # test_instance.example must be replaced
  1531  -/+ resource "test_instance" "example" {
  1532          ami       = "ami-STATIC"
  1533        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1534        ~ set_field = [ # forces replacement
  1535              "aaaa",
  1536            + "bbbb",
  1537              "cccc",
  1538          ]
  1539      }
  1540  `,
  1541  		},
  1542  		"in-place update - deletion": {
  1543  			Action: plans.Update,
  1544  			Mode:   addrs.ManagedResourceMode,
  1545  			Before: cty.ObjectVal(map[string]cty.Value{
  1546  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1547  				"ami": cty.StringVal("ami-STATIC"),
  1548  				"set_field": cty.SetVal([]cty.Value{
  1549  					cty.StringVal("aaaa"),
  1550  					cty.StringVal("bbbb"),
  1551  					cty.StringVal("cccc"),
  1552  				}),
  1553  			}),
  1554  			After: cty.ObjectVal(map[string]cty.Value{
  1555  				"id":  cty.UnknownVal(cty.String),
  1556  				"ami": cty.StringVal("ami-STATIC"),
  1557  				"set_field": cty.SetVal([]cty.Value{
  1558  					cty.StringVal("bbbb"),
  1559  				}),
  1560  			}),
  1561  			Schema: &configschema.Block{
  1562  				Attributes: map[string]*configschema.Attribute{
  1563  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1564  					"ami":       {Type: cty.String, Optional: true},
  1565  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  1566  				},
  1567  			},
  1568  			RequiredReplace: cty.NewPathSet(),
  1569  			Tainted:         false,
  1570  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1571    ~ resource "test_instance" "example" {
  1572          ami       = "ami-STATIC"
  1573        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1574        ~ set_field = [
  1575            - "aaaa",
  1576              "bbbb",
  1577            - "cccc",
  1578          ]
  1579      }
  1580  `,
  1581  		},
  1582  		"creation - empty set": {
  1583  			Action: plans.Create,
  1584  			Mode:   addrs.ManagedResourceMode,
  1585  			Before: cty.NullVal(cty.EmptyObject),
  1586  			After: cty.ObjectVal(map[string]cty.Value{
  1587  				"id":        cty.UnknownVal(cty.String),
  1588  				"ami":       cty.StringVal("ami-STATIC"),
  1589  				"set_field": cty.SetValEmpty(cty.String),
  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  			Tainted:         false,
  1600  			ExpectedOutput: `  # test_instance.example will be created
  1601    + resource "test_instance" "example" {
  1602        + ami       = "ami-STATIC"
  1603        + id        = (known after apply)
  1604        + set_field = []
  1605      }
  1606  `,
  1607  		},
  1608  		"in-place update - full to empty set": {
  1609  			Action: plans.Update,
  1610  			Mode:   addrs.ManagedResourceMode,
  1611  			Before: cty.ObjectVal(map[string]cty.Value{
  1612  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1613  				"ami": cty.StringVal("ami-STATIC"),
  1614  				"set_field": cty.SetVal([]cty.Value{
  1615  					cty.StringVal("aaaa"),
  1616  					cty.StringVal("bbbb"),
  1617  				}),
  1618  			}),
  1619  			After: cty.ObjectVal(map[string]cty.Value{
  1620  				"id":        cty.UnknownVal(cty.String),
  1621  				"ami":       cty.StringVal("ami-STATIC"),
  1622  				"set_field": cty.SetValEmpty(cty.String),
  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          ami       = "ami-STATIC"
  1635        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1636        ~ set_field = [
  1637            - "aaaa",
  1638            - "bbbb",
  1639          ]
  1640      }
  1641  `,
  1642  		},
  1643  		"in-place update - null to empty set": {
  1644  			Action: plans.Update,
  1645  			Mode:   addrs.ManagedResourceMode,
  1646  			Before: cty.ObjectVal(map[string]cty.Value{
  1647  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
  1648  				"ami":       cty.StringVal("ami-STATIC"),
  1649  				"set_field": cty.NullVal(cty.Set(cty.String)),
  1650  			}),
  1651  			After: cty.ObjectVal(map[string]cty.Value{
  1652  				"id":        cty.UnknownVal(cty.String),
  1653  				"ami":       cty.StringVal("ami-STATIC"),
  1654  				"set_field": cty.SetValEmpty(cty.String),
  1655  			}),
  1656  			Schema: &configschema.Block{
  1657  				Attributes: map[string]*configschema.Attribute{
  1658  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1659  					"ami":       {Type: cty.String, Optional: true},
  1660  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  1661  				},
  1662  			},
  1663  			RequiredReplace: cty.NewPathSet(),
  1664  			Tainted:         false,
  1665  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1666    ~ resource "test_instance" "example" {
  1667          ami       = "ami-STATIC"
  1668        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1669        + set_field = []
  1670      }
  1671  `,
  1672  		},
  1673  		"in-place update to unknown": {
  1674  			Action: plans.Update,
  1675  			Mode:   addrs.ManagedResourceMode,
  1676  			Before: cty.ObjectVal(map[string]cty.Value{
  1677  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1678  				"ami": cty.StringVal("ami-STATIC"),
  1679  				"set_field": cty.SetVal([]cty.Value{
  1680  					cty.StringVal("aaaa"),
  1681  					cty.StringVal("bbbb"),
  1682  				}),
  1683  			}),
  1684  			After: cty.ObjectVal(map[string]cty.Value{
  1685  				"id":        cty.UnknownVal(cty.String),
  1686  				"ami":       cty.StringVal("ami-STATIC"),
  1687  				"set_field": cty.UnknownVal(cty.Set(cty.String)),
  1688  			}),
  1689  			Schema: &configschema.Block{
  1690  				Attributes: map[string]*configschema.Attribute{
  1691  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1692  					"ami":       {Type: cty.String, Optional: true},
  1693  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  1694  				},
  1695  			},
  1696  			RequiredReplace: cty.NewPathSet(),
  1697  			Tainted:         false,
  1698  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1699    ~ resource "test_instance" "example" {
  1700          ami       = "ami-STATIC"
  1701        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1702        ~ set_field = [
  1703            - "aaaa",
  1704            - "bbbb",
  1705          ] -> (known after apply)
  1706      }
  1707  `,
  1708  		},
  1709  		"in-place update to unknown element": {
  1710  			Action: plans.Update,
  1711  			Mode:   addrs.ManagedResourceMode,
  1712  			Before: cty.ObjectVal(map[string]cty.Value{
  1713  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1714  				"ami": cty.StringVal("ami-STATIC"),
  1715  				"set_field": cty.SetVal([]cty.Value{
  1716  					cty.StringVal("aaaa"),
  1717  					cty.StringVal("bbbb"),
  1718  				}),
  1719  			}),
  1720  			After: cty.ObjectVal(map[string]cty.Value{
  1721  				"id":  cty.UnknownVal(cty.String),
  1722  				"ami": cty.StringVal("ami-STATIC"),
  1723  				"set_field": cty.SetVal([]cty.Value{
  1724  					cty.StringVal("aaaa"),
  1725  					cty.UnknownVal(cty.String),
  1726  				}),
  1727  			}),
  1728  			Schema: &configschema.Block{
  1729  				Attributes: map[string]*configschema.Attribute{
  1730  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1731  					"ami":       {Type: cty.String, Optional: true},
  1732  					"set_field": {Type: cty.Set(cty.String), Optional: true},
  1733  				},
  1734  			},
  1735  			RequiredReplace: cty.NewPathSet(),
  1736  			Tainted:         false,
  1737  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1738    ~ resource "test_instance" "example" {
  1739          ami       = "ami-STATIC"
  1740        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1741        ~ set_field = [
  1742              "aaaa",
  1743            - "bbbb",
  1744            ~ (known after apply),
  1745          ]
  1746      }
  1747  `,
  1748  		},
  1749  	}
  1750  	runTestCases(t, testCases)
  1751  }
  1752  
  1753  func TestResourceChange_map(t *testing.T) {
  1754  	testCases := map[string]testCase{
  1755  		"in-place update - creation": {
  1756  			Action: plans.Update,
  1757  			Mode:   addrs.ManagedResourceMode,
  1758  			Before: cty.ObjectVal(map[string]cty.Value{
  1759  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
  1760  				"ami":       cty.StringVal("ami-STATIC"),
  1761  				"map_field": cty.NullVal(cty.Map(cty.String)),
  1762  			}),
  1763  			After: cty.ObjectVal(map[string]cty.Value{
  1764  				"id":  cty.UnknownVal(cty.String),
  1765  				"ami": cty.StringVal("ami-STATIC"),
  1766  				"map_field": cty.MapVal(map[string]cty.Value{
  1767  					"new-key": cty.StringVal("new-element"),
  1768  				}),
  1769  			}),
  1770  			Schema: &configschema.Block{
  1771  				Attributes: map[string]*configschema.Attribute{
  1772  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1773  					"ami":       {Type: cty.String, Optional: true},
  1774  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  1775  				},
  1776  			},
  1777  			RequiredReplace: cty.NewPathSet(),
  1778  			Tainted:         false,
  1779  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1780    ~ resource "test_instance" "example" {
  1781          ami       = "ami-STATIC"
  1782        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1783        + map_field = {
  1784            + "new-key" = "new-element"
  1785          }
  1786      }
  1787  `,
  1788  		},
  1789  		"in-place update - first insertion": {
  1790  			Action: plans.Update,
  1791  			Mode:   addrs.ManagedResourceMode,
  1792  			Before: cty.ObjectVal(map[string]cty.Value{
  1793  				"id":        cty.StringVal("i-02ae66f368e8518a9"),
  1794  				"ami":       cty.StringVal("ami-STATIC"),
  1795  				"map_field": cty.MapValEmpty(cty.String),
  1796  			}),
  1797  			After: cty.ObjectVal(map[string]cty.Value{
  1798  				"id":  cty.UnknownVal(cty.String),
  1799  				"ami": cty.StringVal("ami-STATIC"),
  1800  				"map_field": cty.MapVal(map[string]cty.Value{
  1801  					"new-key": cty.StringVal("new-element"),
  1802  				}),
  1803  			}),
  1804  			Schema: &configschema.Block{
  1805  				Attributes: map[string]*configschema.Attribute{
  1806  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1807  					"ami":       {Type: cty.String, Optional: true},
  1808  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  1809  				},
  1810  			},
  1811  			RequiredReplace: cty.NewPathSet(),
  1812  			Tainted:         false,
  1813  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1814    ~ resource "test_instance" "example" {
  1815          ami       = "ami-STATIC"
  1816        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1817        ~ map_field = {
  1818            + "new-key" = "new-element"
  1819          }
  1820      }
  1821  `,
  1822  		},
  1823  		"in-place update - insertion": {
  1824  			Action: plans.Update,
  1825  			Mode:   addrs.ManagedResourceMode,
  1826  			Before: cty.ObjectVal(map[string]cty.Value{
  1827  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1828  				"ami": cty.StringVal("ami-STATIC"),
  1829  				"map_field": cty.MapVal(map[string]cty.Value{
  1830  					"a": cty.StringVal("aaaa"),
  1831  					"c": cty.StringVal("cccc"),
  1832  				}),
  1833  			}),
  1834  			After: cty.ObjectVal(map[string]cty.Value{
  1835  				"id":  cty.UnknownVal(cty.String),
  1836  				"ami": cty.StringVal("ami-STATIC"),
  1837  				"map_field": cty.MapVal(map[string]cty.Value{
  1838  					"a": cty.StringVal("aaaa"),
  1839  					"b": cty.StringVal("bbbb"),
  1840  					"c": cty.StringVal("cccc"),
  1841  				}),
  1842  			}),
  1843  			Schema: &configschema.Block{
  1844  				Attributes: map[string]*configschema.Attribute{
  1845  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1846  					"ami":       {Type: cty.String, Optional: true},
  1847  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  1848  				},
  1849  			},
  1850  			RequiredReplace: cty.NewPathSet(),
  1851  			Tainted:         false,
  1852  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1853    ~ resource "test_instance" "example" {
  1854          ami       = "ami-STATIC"
  1855        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1856        ~ map_field = {
  1857              "a" = "aaaa"
  1858            + "b" = "bbbb"
  1859              "c" = "cccc"
  1860          }
  1861      }
  1862  `,
  1863  		},
  1864  		"force-new update - insertion": {
  1865  			Action: plans.DeleteThenCreate,
  1866  			Mode:   addrs.ManagedResourceMode,
  1867  			Before: cty.ObjectVal(map[string]cty.Value{
  1868  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1869  				"ami": cty.StringVal("ami-STATIC"),
  1870  				"map_field": cty.MapVal(map[string]cty.Value{
  1871  					"a": cty.StringVal("aaaa"),
  1872  					"c": cty.StringVal("cccc"),
  1873  				}),
  1874  			}),
  1875  			After: cty.ObjectVal(map[string]cty.Value{
  1876  				"id":  cty.UnknownVal(cty.String),
  1877  				"ami": cty.StringVal("ami-STATIC"),
  1878  				"map_field": cty.MapVal(map[string]cty.Value{
  1879  					"a": cty.StringVal("aaaa"),
  1880  					"b": cty.StringVal("bbbb"),
  1881  					"c": cty.StringVal("cccc"),
  1882  				}),
  1883  			}),
  1884  			Schema: &configschema.Block{
  1885  				Attributes: map[string]*configschema.Attribute{
  1886  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1887  					"ami":       {Type: cty.String, Optional: true},
  1888  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  1889  				},
  1890  			},
  1891  			RequiredReplace: cty.NewPathSet(cty.Path{
  1892  				cty.GetAttrStep{Name: "map_field"},
  1893  			}),
  1894  			Tainted: false,
  1895  			ExpectedOutput: `  # test_instance.example must be replaced
  1896  -/+ resource "test_instance" "example" {
  1897          ami       = "ami-STATIC"
  1898        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1899        ~ map_field = { # forces replacement
  1900              "a" = "aaaa"
  1901            + "b" = "bbbb"
  1902              "c" = "cccc"
  1903          }
  1904      }
  1905  `,
  1906  		},
  1907  		"in-place update - deletion": {
  1908  			Action: plans.Update,
  1909  			Mode:   addrs.ManagedResourceMode,
  1910  			Before: cty.ObjectVal(map[string]cty.Value{
  1911  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1912  				"ami": cty.StringVal("ami-STATIC"),
  1913  				"map_field": cty.MapVal(map[string]cty.Value{
  1914  					"a": cty.StringVal("aaaa"),
  1915  					"b": cty.StringVal("bbbb"),
  1916  					"c": cty.StringVal("cccc"),
  1917  				}),
  1918  			}),
  1919  			After: cty.ObjectVal(map[string]cty.Value{
  1920  				"id":  cty.UnknownVal(cty.String),
  1921  				"ami": cty.StringVal("ami-STATIC"),
  1922  				"map_field": cty.MapVal(map[string]cty.Value{
  1923  					"b": cty.StringVal("bbbb"),
  1924  				}),
  1925  			}),
  1926  			Schema: &configschema.Block{
  1927  				Attributes: map[string]*configschema.Attribute{
  1928  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1929  					"ami":       {Type: cty.String, Optional: true},
  1930  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  1931  				},
  1932  			},
  1933  			RequiredReplace: cty.NewPathSet(),
  1934  			Tainted:         false,
  1935  			ExpectedOutput: `  # test_instance.example will be updated in-place
  1936    ~ resource "test_instance" "example" {
  1937          ami       = "ami-STATIC"
  1938        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  1939        ~ map_field = {
  1940            - "a" = "aaaa" -> null
  1941              "b" = "bbbb"
  1942            - "c" = "cccc" -> null
  1943          }
  1944      }
  1945  `,
  1946  		},
  1947  		"creation - empty": {
  1948  			Action: plans.Create,
  1949  			Mode:   addrs.ManagedResourceMode,
  1950  			Before: cty.NullVal(cty.EmptyObject),
  1951  			After: cty.ObjectVal(map[string]cty.Value{
  1952  				"id":        cty.UnknownVal(cty.String),
  1953  				"ami":       cty.StringVal("ami-STATIC"),
  1954  				"map_field": cty.MapValEmpty(cty.String),
  1955  			}),
  1956  			Schema: &configschema.Block{
  1957  				Attributes: map[string]*configschema.Attribute{
  1958  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1959  					"ami":       {Type: cty.String, Optional: true},
  1960  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  1961  				},
  1962  			},
  1963  			RequiredReplace: cty.NewPathSet(),
  1964  			Tainted:         false,
  1965  			ExpectedOutput: `  # test_instance.example will be created
  1966    + resource "test_instance" "example" {
  1967        + ami       = "ami-STATIC"
  1968        + id        = (known after apply)
  1969        + map_field = {}
  1970      }
  1971  `,
  1972  		},
  1973  		"update to unknown element": {
  1974  			Action: plans.Update,
  1975  			Mode:   addrs.ManagedResourceMode,
  1976  			Before: cty.ObjectVal(map[string]cty.Value{
  1977  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  1978  				"ami": cty.StringVal("ami-STATIC"),
  1979  				"map_field": cty.MapVal(map[string]cty.Value{
  1980  					"a": cty.StringVal("aaaa"),
  1981  					"b": cty.StringVal("bbbb"),
  1982  					"c": cty.StringVal("cccc"),
  1983  				}),
  1984  			}),
  1985  			After: cty.ObjectVal(map[string]cty.Value{
  1986  				"id":  cty.UnknownVal(cty.String),
  1987  				"ami": cty.StringVal("ami-STATIC"),
  1988  				"map_field": cty.MapVal(map[string]cty.Value{
  1989  					"a": cty.StringVal("aaaa"),
  1990  					"b": cty.UnknownVal(cty.String),
  1991  					"c": cty.StringVal("cccc"),
  1992  				}),
  1993  			}),
  1994  			Schema: &configschema.Block{
  1995  				Attributes: map[string]*configschema.Attribute{
  1996  					"id":        {Type: cty.String, Optional: true, Computed: true},
  1997  					"ami":       {Type: cty.String, Optional: true},
  1998  					"map_field": {Type: cty.Map(cty.String), Optional: true},
  1999  				},
  2000  			},
  2001  			RequiredReplace: cty.NewPathSet(),
  2002  			Tainted:         false,
  2003  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2004    ~ resource "test_instance" "example" {
  2005          ami       = "ami-STATIC"
  2006        ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
  2007        ~ map_field = {
  2008              "a" = "aaaa"
  2009            ~ "b" = "bbbb" -> (known after apply)
  2010              "c" = "cccc"
  2011          }
  2012      }
  2013  `,
  2014  		},
  2015  	}
  2016  	runTestCases(t, testCases)
  2017  }
  2018  
  2019  func TestResourceChange_nestedList(t *testing.T) {
  2020  	testCases := map[string]testCase{
  2021  		"in-place update - equal": {
  2022  			Action: plans.Update,
  2023  			Mode:   addrs.ManagedResourceMode,
  2024  			Before: cty.ObjectVal(map[string]cty.Value{
  2025  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2026  				"ami": cty.StringVal("ami-BEFORE"),
  2027  				"root_block_device": cty.ListVal([]cty.Value{
  2028  					cty.ObjectVal(map[string]cty.Value{
  2029  						"volume_type": cty.StringVal("gp2"),
  2030  					}),
  2031  				}),
  2032  			}),
  2033  			After: cty.ObjectVal(map[string]cty.Value{
  2034  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2035  				"ami": cty.StringVal("ami-AFTER"),
  2036  				"root_block_device": cty.ListVal([]cty.Value{
  2037  					cty.ObjectVal(map[string]cty.Value{
  2038  						"volume_type": cty.StringVal("gp2"),
  2039  					}),
  2040  				}),
  2041  			}),
  2042  			RequiredReplace: cty.NewPathSet(),
  2043  			Tainted:         false,
  2044  			Schema: &configschema.Block{
  2045  				Attributes: map[string]*configschema.Attribute{
  2046  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2047  					"ami": {Type: cty.String, Optional: true},
  2048  				},
  2049  				BlockTypes: map[string]*configschema.NestedBlock{
  2050  					"root_block_device": {
  2051  						Block: configschema.Block{
  2052  							Attributes: map[string]*configschema.Attribute{
  2053  								"volume_type": {
  2054  									Type:     cty.String,
  2055  									Optional: true,
  2056  									Computed: true,
  2057  								},
  2058  							},
  2059  						},
  2060  						Nesting: configschema.NestingList,
  2061  					},
  2062  				},
  2063  			},
  2064  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2065    ~ resource "test_instance" "example" {
  2066        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2067          id  = "i-02ae66f368e8518a9"
  2068  
  2069          root_block_device {
  2070              volume_type = "gp2"
  2071          }
  2072      }
  2073  `,
  2074  		},
  2075  		"in-place update - creation": {
  2076  			Action: plans.Update,
  2077  			Mode:   addrs.ManagedResourceMode,
  2078  			Before: cty.ObjectVal(map[string]cty.Value{
  2079  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2080  				"ami": cty.StringVal("ami-BEFORE"),
  2081  				"root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  2082  					"volume_type": cty.String,
  2083  				})),
  2084  			}),
  2085  			After: cty.ObjectVal(map[string]cty.Value{
  2086  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2087  				"ami": cty.StringVal("ami-AFTER"),
  2088  				"root_block_device": cty.ListVal([]cty.Value{
  2089  					cty.ObjectVal(map[string]cty.Value{
  2090  						"volume_type": cty.NullVal(cty.String),
  2091  					}),
  2092  				}),
  2093  			}),
  2094  			RequiredReplace: cty.NewPathSet(),
  2095  			Tainted:         false,
  2096  			Schema: &configschema.Block{
  2097  				Attributes: map[string]*configschema.Attribute{
  2098  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2099  					"ami": {Type: cty.String, Optional: true},
  2100  				},
  2101  				BlockTypes: map[string]*configschema.NestedBlock{
  2102  					"root_block_device": {
  2103  						Block: configschema.Block{
  2104  							Attributes: map[string]*configschema.Attribute{
  2105  								"volume_type": {
  2106  									Type:     cty.String,
  2107  									Optional: true,
  2108  									Computed: true,
  2109  								},
  2110  							},
  2111  						},
  2112  						Nesting: configschema.NestingList,
  2113  					},
  2114  				},
  2115  			},
  2116  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2117    ~ resource "test_instance" "example" {
  2118        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2119          id  = "i-02ae66f368e8518a9"
  2120  
  2121        + root_block_device {}
  2122      }
  2123  `,
  2124  		},
  2125  		"in-place update - first insertion": {
  2126  			Action: plans.Update,
  2127  			Mode:   addrs.ManagedResourceMode,
  2128  			Before: cty.ObjectVal(map[string]cty.Value{
  2129  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2130  				"ami": cty.StringVal("ami-BEFORE"),
  2131  				"root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  2132  					"volume_type": cty.String,
  2133  				})),
  2134  			}),
  2135  			After: cty.ObjectVal(map[string]cty.Value{
  2136  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2137  				"ami": cty.StringVal("ami-AFTER"),
  2138  				"root_block_device": cty.ListVal([]cty.Value{
  2139  					cty.ObjectVal(map[string]cty.Value{
  2140  						"volume_type": cty.StringVal("gp2"),
  2141  					}),
  2142  				}),
  2143  			}),
  2144  			RequiredReplace: cty.NewPathSet(),
  2145  			Tainted:         false,
  2146  			Schema: &configschema.Block{
  2147  				Attributes: map[string]*configschema.Attribute{
  2148  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2149  					"ami": {Type: cty.String, Optional: true},
  2150  				},
  2151  				BlockTypes: map[string]*configschema.NestedBlock{
  2152  					"root_block_device": {
  2153  						Block: configschema.Block{
  2154  							Attributes: map[string]*configschema.Attribute{
  2155  								"volume_type": {
  2156  									Type:     cty.String,
  2157  									Optional: true,
  2158  									Computed: true,
  2159  								},
  2160  							},
  2161  						},
  2162  						Nesting: configschema.NestingList,
  2163  					},
  2164  				},
  2165  			},
  2166  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2167    ~ resource "test_instance" "example" {
  2168        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2169          id  = "i-02ae66f368e8518a9"
  2170  
  2171        + root_block_device {
  2172            + volume_type = "gp2"
  2173          }
  2174      }
  2175  `,
  2176  		},
  2177  		"in-place update - insertion": {
  2178  			Action: plans.Update,
  2179  			Mode:   addrs.ManagedResourceMode,
  2180  			Before: cty.ObjectVal(map[string]cty.Value{
  2181  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2182  				"ami": cty.StringVal("ami-BEFORE"),
  2183  				"root_block_device": cty.ListVal([]cty.Value{
  2184  					cty.ObjectVal(map[string]cty.Value{
  2185  						"volume_type": cty.StringVal("gp2"),
  2186  						"new_field":   cty.NullVal(cty.String),
  2187  					}),
  2188  				}),
  2189  			}),
  2190  			After: cty.ObjectVal(map[string]cty.Value{
  2191  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2192  				"ami": cty.StringVal("ami-AFTER"),
  2193  				"root_block_device": cty.ListVal([]cty.Value{
  2194  					cty.ObjectVal(map[string]cty.Value{
  2195  						"volume_type": cty.StringVal("gp2"),
  2196  						"new_field":   cty.StringVal("new_value"),
  2197  					}),
  2198  				}),
  2199  			}),
  2200  			RequiredReplace: cty.NewPathSet(),
  2201  			Tainted:         false,
  2202  			Schema: &configschema.Block{
  2203  				Attributes: map[string]*configschema.Attribute{
  2204  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2205  					"ami": {Type: cty.String, Optional: true},
  2206  				},
  2207  				BlockTypes: map[string]*configschema.NestedBlock{
  2208  					"root_block_device": {
  2209  						Block: configschema.Block{
  2210  							Attributes: map[string]*configschema.Attribute{
  2211  								"volume_type": {
  2212  									Type:     cty.String,
  2213  									Optional: true,
  2214  									Computed: true,
  2215  								},
  2216  								"new_field": {
  2217  									Type:     cty.String,
  2218  									Optional: true,
  2219  									Computed: true,
  2220  								},
  2221  							},
  2222  						},
  2223  						Nesting: configschema.NestingList,
  2224  					},
  2225  				},
  2226  			},
  2227  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2228    ~ resource "test_instance" "example" {
  2229        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2230          id  = "i-02ae66f368e8518a9"
  2231  
  2232        ~ root_block_device {
  2233            + new_field   = "new_value"
  2234              volume_type = "gp2"
  2235          }
  2236      }
  2237  `,
  2238  		},
  2239  		"force-new update (inside block)": {
  2240  			Action: plans.DeleteThenCreate,
  2241  			Mode:   addrs.ManagedResourceMode,
  2242  			Before: cty.ObjectVal(map[string]cty.Value{
  2243  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2244  				"ami": cty.StringVal("ami-BEFORE"),
  2245  				"root_block_device": cty.ListVal([]cty.Value{
  2246  					cty.ObjectVal(map[string]cty.Value{
  2247  						"volume_type": cty.StringVal("gp2"),
  2248  					}),
  2249  				}),
  2250  			}),
  2251  			After: cty.ObjectVal(map[string]cty.Value{
  2252  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2253  				"ami": cty.StringVal("ami-AFTER"),
  2254  				"root_block_device": cty.ListVal([]cty.Value{
  2255  					cty.ObjectVal(map[string]cty.Value{
  2256  						"volume_type": cty.StringVal("different"),
  2257  					}),
  2258  				}),
  2259  			}),
  2260  			RequiredReplace: cty.NewPathSet(cty.Path{
  2261  				cty.GetAttrStep{Name: "root_block_device"},
  2262  				cty.IndexStep{Key: cty.NumberIntVal(0)},
  2263  				cty.GetAttrStep{Name: "volume_type"},
  2264  			}),
  2265  			Tainted: false,
  2266  			Schema: &configschema.Block{
  2267  				Attributes: map[string]*configschema.Attribute{
  2268  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2269  					"ami": {Type: cty.String, Optional: true},
  2270  				},
  2271  				BlockTypes: map[string]*configschema.NestedBlock{
  2272  					"root_block_device": {
  2273  						Block: configschema.Block{
  2274  							Attributes: map[string]*configschema.Attribute{
  2275  								"volume_type": {
  2276  									Type:     cty.String,
  2277  									Optional: true,
  2278  									Computed: true,
  2279  								},
  2280  							},
  2281  						},
  2282  						Nesting: configschema.NestingList,
  2283  					},
  2284  				},
  2285  			},
  2286  			ExpectedOutput: `  # test_instance.example must be replaced
  2287  -/+ resource "test_instance" "example" {
  2288        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2289          id  = "i-02ae66f368e8518a9"
  2290  
  2291        ~ root_block_device {
  2292            ~ volume_type = "gp2" -> "different" # forces replacement
  2293          }
  2294      }
  2295  `,
  2296  		},
  2297  		"force-new update (whole block)": {
  2298  			Action: plans.DeleteThenCreate,
  2299  			Mode:   addrs.ManagedResourceMode,
  2300  			Before: cty.ObjectVal(map[string]cty.Value{
  2301  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2302  				"ami": cty.StringVal("ami-BEFORE"),
  2303  				"root_block_device": cty.ListVal([]cty.Value{
  2304  					cty.ObjectVal(map[string]cty.Value{
  2305  						"volume_type": cty.StringVal("gp2"),
  2306  					}),
  2307  				}),
  2308  			}),
  2309  			After: cty.ObjectVal(map[string]cty.Value{
  2310  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2311  				"ami": cty.StringVal("ami-AFTER"),
  2312  				"root_block_device": cty.ListVal([]cty.Value{
  2313  					cty.ObjectVal(map[string]cty.Value{
  2314  						"volume_type": cty.StringVal("different"),
  2315  					}),
  2316  				}),
  2317  			}),
  2318  			RequiredReplace: cty.NewPathSet(cty.Path{
  2319  				cty.GetAttrStep{Name: "root_block_device"},
  2320  			}),
  2321  			Tainted: false,
  2322  			Schema: &configschema.Block{
  2323  				Attributes: map[string]*configschema.Attribute{
  2324  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2325  					"ami": {Type: cty.String, Optional: true},
  2326  				},
  2327  				BlockTypes: map[string]*configschema.NestedBlock{
  2328  					"root_block_device": {
  2329  						Block: configschema.Block{
  2330  							Attributes: map[string]*configschema.Attribute{
  2331  								"volume_type": {
  2332  									Type:     cty.String,
  2333  									Optional: true,
  2334  									Computed: true,
  2335  								},
  2336  							},
  2337  						},
  2338  						Nesting: configschema.NestingList,
  2339  					},
  2340  				},
  2341  			},
  2342  			ExpectedOutput: `  # test_instance.example must be replaced
  2343  -/+ resource "test_instance" "example" {
  2344        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2345          id  = "i-02ae66f368e8518a9"
  2346  
  2347        ~ root_block_device { # forces replacement
  2348            ~ volume_type = "gp2" -> "different"
  2349          }
  2350      }
  2351  `,
  2352  		},
  2353  		"in-place update - deletion": {
  2354  			Action: plans.Update,
  2355  			Mode:   addrs.ManagedResourceMode,
  2356  			Before: cty.ObjectVal(map[string]cty.Value{
  2357  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2358  				"ami": cty.StringVal("ami-BEFORE"),
  2359  				"root_block_device": cty.ListVal([]cty.Value{
  2360  					cty.ObjectVal(map[string]cty.Value{
  2361  						"volume_type": cty.StringVal("gp2"),
  2362  						"new_field":   cty.StringVal("new_value"),
  2363  					}),
  2364  				}),
  2365  			}),
  2366  			After: cty.ObjectVal(map[string]cty.Value{
  2367  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2368  				"ami": cty.StringVal("ami-AFTER"),
  2369  				"root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{
  2370  					"volume_type": cty.String,
  2371  					"new_field":   cty.String,
  2372  				})),
  2373  			}),
  2374  			RequiredReplace: cty.NewPathSet(),
  2375  			Tainted:         false,
  2376  			Schema: &configschema.Block{
  2377  				Attributes: map[string]*configschema.Attribute{
  2378  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2379  					"ami": {Type: cty.String, Optional: true},
  2380  				},
  2381  				BlockTypes: map[string]*configschema.NestedBlock{
  2382  					"root_block_device": {
  2383  						Block: configschema.Block{
  2384  							Attributes: map[string]*configschema.Attribute{
  2385  								"volume_type": {
  2386  									Type:     cty.String,
  2387  									Optional: true,
  2388  									Computed: true,
  2389  								},
  2390  								"new_field": {
  2391  									Type:     cty.String,
  2392  									Optional: true,
  2393  									Computed: true,
  2394  								},
  2395  							},
  2396  						},
  2397  						Nesting: configschema.NestingList,
  2398  					},
  2399  				},
  2400  			},
  2401  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2402    ~ resource "test_instance" "example" {
  2403        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2404          id  = "i-02ae66f368e8518a9"
  2405  
  2406        - root_block_device {
  2407            - new_field   = "new_value" -> null
  2408            - volume_type = "gp2" -> null
  2409          }
  2410      }
  2411  `,
  2412  		},
  2413  		"with dynamically-typed attribute": {
  2414  			Action: plans.Update,
  2415  			Mode:   addrs.ManagedResourceMode,
  2416  			Before: cty.ObjectVal(map[string]cty.Value{
  2417  				"block": cty.EmptyTupleVal,
  2418  			}),
  2419  			After: cty.ObjectVal(map[string]cty.Value{
  2420  				"block": cty.TupleVal([]cty.Value{
  2421  					cty.ObjectVal(map[string]cty.Value{
  2422  						"attr": cty.StringVal("foo"),
  2423  					}),
  2424  					cty.ObjectVal(map[string]cty.Value{
  2425  						"attr": cty.True,
  2426  					}),
  2427  				}),
  2428  			}),
  2429  			RequiredReplace: cty.NewPathSet(),
  2430  			Tainted:         false,
  2431  			Schema: &configschema.Block{
  2432  				BlockTypes: map[string]*configschema.NestedBlock{
  2433  					"block": {
  2434  						Block: configschema.Block{
  2435  							Attributes: map[string]*configschema.Attribute{
  2436  								"attr": {Type: cty.DynamicPseudoType, Optional: true},
  2437  							},
  2438  						},
  2439  						Nesting: configschema.NestingList,
  2440  					},
  2441  				},
  2442  			},
  2443  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2444    ~ resource "test_instance" "example" {
  2445        + block {
  2446            + attr = "foo"
  2447          }
  2448        + block {
  2449            + attr = true
  2450          }
  2451      }
  2452  `,
  2453  		},
  2454  	}
  2455  	runTestCases(t, testCases)
  2456  }
  2457  
  2458  func TestResourceChange_nestedSet(t *testing.T) {
  2459  	testCases := map[string]testCase{
  2460  		"in-place update - creation": {
  2461  			Action: plans.Update,
  2462  			Mode:   addrs.ManagedResourceMode,
  2463  			Before: cty.ObjectVal(map[string]cty.Value{
  2464  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2465  				"ami": cty.StringVal("ami-BEFORE"),
  2466  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  2467  					"volume_type": cty.String,
  2468  				})),
  2469  			}),
  2470  			After: cty.ObjectVal(map[string]cty.Value{
  2471  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2472  				"ami": cty.StringVal("ami-AFTER"),
  2473  				"root_block_device": cty.SetVal([]cty.Value{
  2474  					cty.ObjectVal(map[string]cty.Value{
  2475  						"volume_type": cty.StringVal("gp2"),
  2476  					}),
  2477  				}),
  2478  			}),
  2479  			RequiredReplace: cty.NewPathSet(),
  2480  			Tainted:         false,
  2481  			Schema: &configschema.Block{
  2482  				Attributes: map[string]*configschema.Attribute{
  2483  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2484  					"ami": {Type: cty.String, Optional: true},
  2485  				},
  2486  				BlockTypes: map[string]*configschema.NestedBlock{
  2487  					"root_block_device": {
  2488  						Block: configschema.Block{
  2489  							Attributes: map[string]*configschema.Attribute{
  2490  								"volume_type": {
  2491  									Type:     cty.String,
  2492  									Optional: true,
  2493  									Computed: true,
  2494  								},
  2495  							},
  2496  						},
  2497  						Nesting: configschema.NestingSet,
  2498  					},
  2499  				},
  2500  			},
  2501  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2502    ~ resource "test_instance" "example" {
  2503        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2504          id  = "i-02ae66f368e8518a9"
  2505  
  2506        + root_block_device {
  2507            + volume_type = "gp2"
  2508          }
  2509      }
  2510  `,
  2511  		},
  2512  		"in-place update - insertion": {
  2513  			Action: plans.Update,
  2514  			Mode:   addrs.ManagedResourceMode,
  2515  			Before: cty.ObjectVal(map[string]cty.Value{
  2516  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2517  				"ami": cty.StringVal("ami-BEFORE"),
  2518  				"root_block_device": cty.SetVal([]cty.Value{
  2519  					cty.ObjectVal(map[string]cty.Value{
  2520  						"volume_type": cty.StringVal("gp2"),
  2521  						"new_field":   cty.NullVal(cty.String),
  2522  					}),
  2523  				}),
  2524  			}),
  2525  			After: cty.ObjectVal(map[string]cty.Value{
  2526  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2527  				"ami": cty.StringVal("ami-AFTER"),
  2528  				"root_block_device": cty.SetVal([]cty.Value{
  2529  					cty.ObjectVal(map[string]cty.Value{
  2530  						"volume_type": cty.StringVal("gp2"),
  2531  						"new_field":   cty.StringVal("new_value"),
  2532  					}),
  2533  				}),
  2534  			}),
  2535  			RequiredReplace: cty.NewPathSet(),
  2536  			Tainted:         false,
  2537  			Schema: &configschema.Block{
  2538  				Attributes: map[string]*configschema.Attribute{
  2539  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2540  					"ami": {Type: cty.String, Optional: true},
  2541  				},
  2542  				BlockTypes: map[string]*configschema.NestedBlock{
  2543  					"root_block_device": {
  2544  						Block: configschema.Block{
  2545  							Attributes: map[string]*configschema.Attribute{
  2546  								"volume_type": {
  2547  									Type:     cty.String,
  2548  									Optional: true,
  2549  									Computed: true,
  2550  								},
  2551  								"new_field": {
  2552  									Type:     cty.String,
  2553  									Optional: true,
  2554  									Computed: true,
  2555  								},
  2556  							},
  2557  						},
  2558  						Nesting: configschema.NestingSet,
  2559  					},
  2560  				},
  2561  			},
  2562  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2563    ~ resource "test_instance" "example" {
  2564        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2565          id  = "i-02ae66f368e8518a9"
  2566  
  2567        + root_block_device {
  2568            + new_field   = "new_value"
  2569            + volume_type = "gp2"
  2570          }
  2571        - root_block_device {
  2572            - volume_type = "gp2" -> null
  2573          }
  2574      }
  2575  `,
  2576  		},
  2577  		"force-new update (whole block)": {
  2578  			Action: plans.DeleteThenCreate,
  2579  			Mode:   addrs.ManagedResourceMode,
  2580  			Before: cty.ObjectVal(map[string]cty.Value{
  2581  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2582  				"ami": cty.StringVal("ami-BEFORE"),
  2583  				"root_block_device": cty.SetVal([]cty.Value{
  2584  					cty.ObjectVal(map[string]cty.Value{
  2585  						"volume_type": cty.StringVal("gp2"),
  2586  					}),
  2587  				}),
  2588  			}),
  2589  			After: cty.ObjectVal(map[string]cty.Value{
  2590  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2591  				"ami": cty.StringVal("ami-AFTER"),
  2592  				"root_block_device": cty.SetVal([]cty.Value{
  2593  					cty.ObjectVal(map[string]cty.Value{
  2594  						"volume_type": cty.StringVal("different"),
  2595  					}),
  2596  				}),
  2597  			}),
  2598  			RequiredReplace: cty.NewPathSet(cty.Path{
  2599  				cty.GetAttrStep{Name: "root_block_device"},
  2600  			}),
  2601  			Tainted: false,
  2602  			Schema: &configschema.Block{
  2603  				Attributes: map[string]*configschema.Attribute{
  2604  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2605  					"ami": {Type: cty.String, Optional: true},
  2606  				},
  2607  				BlockTypes: map[string]*configschema.NestedBlock{
  2608  					"root_block_device": {
  2609  						Block: configschema.Block{
  2610  							Attributes: map[string]*configschema.Attribute{
  2611  								"volume_type": {
  2612  									Type:     cty.String,
  2613  									Optional: true,
  2614  									Computed: true,
  2615  								},
  2616  							},
  2617  						},
  2618  						Nesting: configschema.NestingSet,
  2619  					},
  2620  				},
  2621  			},
  2622  			ExpectedOutput: `  # test_instance.example must be replaced
  2623  -/+ resource "test_instance" "example" {
  2624        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2625          id  = "i-02ae66f368e8518a9"
  2626  
  2627        + root_block_device { # forces replacement
  2628            + volume_type = "different"
  2629          }
  2630        - root_block_device { # forces replacement
  2631            - volume_type = "gp2" -> null
  2632          }
  2633      }
  2634  `,
  2635  		},
  2636  		"in-place update - deletion": {
  2637  			Action: plans.Update,
  2638  			Mode:   addrs.ManagedResourceMode,
  2639  			Before: cty.ObjectVal(map[string]cty.Value{
  2640  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2641  				"ami": cty.StringVal("ami-BEFORE"),
  2642  				"root_block_device": cty.SetVal([]cty.Value{
  2643  					cty.ObjectVal(map[string]cty.Value{
  2644  						"volume_type": cty.StringVal("gp2"),
  2645  						"new_field":   cty.StringVal("new_value"),
  2646  					}),
  2647  				}),
  2648  			}),
  2649  			After: cty.ObjectVal(map[string]cty.Value{
  2650  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2651  				"ami": cty.StringVal("ami-AFTER"),
  2652  				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  2653  					"volume_type": cty.String,
  2654  					"new_field":   cty.String,
  2655  				})),
  2656  			}),
  2657  			RequiredReplace: cty.NewPathSet(),
  2658  			Tainted:         false,
  2659  			Schema: &configschema.Block{
  2660  				Attributes: map[string]*configschema.Attribute{
  2661  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2662  					"ami": {Type: cty.String, Optional: true},
  2663  				},
  2664  				BlockTypes: map[string]*configschema.NestedBlock{
  2665  					"root_block_device": {
  2666  						Block: configschema.Block{
  2667  							Attributes: map[string]*configschema.Attribute{
  2668  								"volume_type": {
  2669  									Type:     cty.String,
  2670  									Optional: true,
  2671  									Computed: true,
  2672  								},
  2673  								"new_field": {
  2674  									Type:     cty.String,
  2675  									Optional: true,
  2676  									Computed: true,
  2677  								},
  2678  							},
  2679  						},
  2680  						Nesting: configschema.NestingSet,
  2681  					},
  2682  				},
  2683  			},
  2684  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2685    ~ resource "test_instance" "example" {
  2686        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2687          id  = "i-02ae66f368e8518a9"
  2688  
  2689        - root_block_device {
  2690            - new_field   = "new_value" -> null
  2691            - volume_type = "gp2" -> null
  2692          }
  2693      }
  2694  `,
  2695  		},
  2696  	}
  2697  	runTestCases(t, testCases)
  2698  }
  2699  
  2700  func TestResourceChange_nestedMap(t *testing.T) {
  2701  	testCases := map[string]testCase{
  2702  		"in-place update - creation": {
  2703  			Action: plans.Update,
  2704  			Mode:   addrs.ManagedResourceMode,
  2705  			Before: cty.ObjectVal(map[string]cty.Value{
  2706  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2707  				"ami": cty.StringVal("ami-BEFORE"),
  2708  				"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  2709  					"volume_type": cty.String,
  2710  				})),
  2711  			}),
  2712  			After: cty.ObjectVal(map[string]cty.Value{
  2713  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2714  				"ami": cty.StringVal("ami-AFTER"),
  2715  				"root_block_device": cty.MapVal(map[string]cty.Value{
  2716  					"a": cty.ObjectVal(map[string]cty.Value{
  2717  						"volume_type": cty.StringVal("gp2"),
  2718  					}),
  2719  				}),
  2720  			}),
  2721  			RequiredReplace: cty.NewPathSet(),
  2722  			Tainted:         false,
  2723  			Schema: &configschema.Block{
  2724  				Attributes: map[string]*configschema.Attribute{
  2725  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2726  					"ami": {Type: cty.String, Optional: true},
  2727  				},
  2728  				BlockTypes: map[string]*configschema.NestedBlock{
  2729  					"root_block_device": {
  2730  						Block: configschema.Block{
  2731  							Attributes: map[string]*configschema.Attribute{
  2732  								"volume_type": {
  2733  									Type:     cty.String,
  2734  									Optional: true,
  2735  									Computed: true,
  2736  								},
  2737  							},
  2738  						},
  2739  						Nesting: configschema.NestingMap,
  2740  					},
  2741  				},
  2742  			},
  2743  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2744    ~ resource "test_instance" "example" {
  2745        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2746          id  = "i-02ae66f368e8518a9"
  2747  
  2748        + root_block_device "a" {
  2749            + volume_type = "gp2"
  2750          }
  2751      }
  2752  `,
  2753  		},
  2754  		"in-place update - change attr": {
  2755  			Action: plans.Update,
  2756  			Mode:   addrs.ManagedResourceMode,
  2757  			Before: cty.ObjectVal(map[string]cty.Value{
  2758  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2759  				"ami": cty.StringVal("ami-BEFORE"),
  2760  				"root_block_device": cty.MapVal(map[string]cty.Value{
  2761  					"a": cty.ObjectVal(map[string]cty.Value{
  2762  						"volume_type": cty.StringVal("gp2"),
  2763  						"new_field":   cty.NullVal(cty.String),
  2764  					}),
  2765  				}),
  2766  			}),
  2767  			After: cty.ObjectVal(map[string]cty.Value{
  2768  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2769  				"ami": cty.StringVal("ami-AFTER"),
  2770  				"root_block_device": cty.MapVal(map[string]cty.Value{
  2771  					"a": cty.ObjectVal(map[string]cty.Value{
  2772  						"volume_type": cty.StringVal("gp2"),
  2773  						"new_field":   cty.StringVal("new_value"),
  2774  					}),
  2775  				}),
  2776  			}),
  2777  			RequiredReplace: cty.NewPathSet(),
  2778  			Tainted:         false,
  2779  			Schema: &configschema.Block{
  2780  				Attributes: map[string]*configschema.Attribute{
  2781  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2782  					"ami": {Type: cty.String, Optional: true},
  2783  				},
  2784  				BlockTypes: map[string]*configschema.NestedBlock{
  2785  					"root_block_device": {
  2786  						Block: configschema.Block{
  2787  							Attributes: map[string]*configschema.Attribute{
  2788  								"volume_type": {
  2789  									Type:     cty.String,
  2790  									Optional: true,
  2791  									Computed: true,
  2792  								},
  2793  								"new_field": {
  2794  									Type:     cty.String,
  2795  									Optional: true,
  2796  									Computed: true,
  2797  								},
  2798  							},
  2799  						},
  2800  						Nesting: configschema.NestingMap,
  2801  					},
  2802  				},
  2803  			},
  2804  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2805    ~ resource "test_instance" "example" {
  2806        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2807          id  = "i-02ae66f368e8518a9"
  2808  
  2809        ~ root_block_device "a" {
  2810            + new_field   = "new_value"
  2811              volume_type = "gp2"
  2812          }
  2813      }
  2814  `,
  2815  		},
  2816  		"in-place update - insertion": {
  2817  			Action: plans.Update,
  2818  			Mode:   addrs.ManagedResourceMode,
  2819  			Before: cty.ObjectVal(map[string]cty.Value{
  2820  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2821  				"ami": cty.StringVal("ami-BEFORE"),
  2822  				"root_block_device": cty.MapVal(map[string]cty.Value{
  2823  					"a": cty.ObjectVal(map[string]cty.Value{
  2824  						"volume_type": cty.StringVal("gp2"),
  2825  						"new_field":   cty.NullVal(cty.String),
  2826  					}),
  2827  				}),
  2828  			}),
  2829  			After: cty.ObjectVal(map[string]cty.Value{
  2830  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2831  				"ami": cty.StringVal("ami-AFTER"),
  2832  				"root_block_device": cty.MapVal(map[string]cty.Value{
  2833  					"a": cty.ObjectVal(map[string]cty.Value{
  2834  						"volume_type": cty.StringVal("gp2"),
  2835  						"new_field":   cty.NullVal(cty.String),
  2836  					}),
  2837  					"b": cty.ObjectVal(map[string]cty.Value{
  2838  						"volume_type": cty.StringVal("gp2"),
  2839  						"new_field":   cty.StringVal("new_value"),
  2840  					}),
  2841  				}),
  2842  			}),
  2843  			RequiredReplace: cty.NewPathSet(),
  2844  			Tainted:         false,
  2845  			Schema: &configschema.Block{
  2846  				Attributes: map[string]*configschema.Attribute{
  2847  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2848  					"ami": {Type: cty.String, Optional: true},
  2849  				},
  2850  				BlockTypes: map[string]*configschema.NestedBlock{
  2851  					"root_block_device": {
  2852  						Block: configschema.Block{
  2853  							Attributes: map[string]*configschema.Attribute{
  2854  								"volume_type": {
  2855  									Type:     cty.String,
  2856  									Optional: true,
  2857  									Computed: true,
  2858  								},
  2859  								"new_field": {
  2860  									Type:     cty.String,
  2861  									Optional: true,
  2862  									Computed: true,
  2863  								},
  2864  							},
  2865  						},
  2866  						Nesting: configschema.NestingMap,
  2867  					},
  2868  				},
  2869  			},
  2870  			ExpectedOutput: `  # test_instance.example will be updated in-place
  2871    ~ resource "test_instance" "example" {
  2872        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2873          id  = "i-02ae66f368e8518a9"
  2874  
  2875          root_block_device "a" {
  2876              volume_type = "gp2"
  2877          }
  2878        + root_block_device "b" {
  2879            + new_field   = "new_value"
  2880            + volume_type = "gp2"
  2881          }
  2882      }
  2883  `,
  2884  		},
  2885  		"force-new update (whole block)": {
  2886  			Action: plans.DeleteThenCreate,
  2887  			Mode:   addrs.ManagedResourceMode,
  2888  			Before: cty.ObjectVal(map[string]cty.Value{
  2889  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2890  				"ami": cty.StringVal("ami-BEFORE"),
  2891  				"root_block_device": cty.MapVal(map[string]cty.Value{
  2892  					"a": cty.ObjectVal(map[string]cty.Value{
  2893  						"volume_type": cty.StringVal("gp2"),
  2894  					}),
  2895  					"b": cty.ObjectVal(map[string]cty.Value{
  2896  						"volume_type": cty.StringVal("standard"),
  2897  					}),
  2898  				}),
  2899  			}),
  2900  			After: cty.ObjectVal(map[string]cty.Value{
  2901  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2902  				"ami": cty.StringVal("ami-AFTER"),
  2903  				"root_block_device": cty.MapVal(map[string]cty.Value{
  2904  					"a": cty.ObjectVal(map[string]cty.Value{
  2905  						"volume_type": cty.StringVal("different"),
  2906  					}),
  2907  					"b": cty.ObjectVal(map[string]cty.Value{
  2908  						"volume_type": cty.StringVal("standard"),
  2909  					}),
  2910  				}),
  2911  			}),
  2912  			RequiredReplace: cty.NewPathSet(cty.Path{
  2913  				cty.GetAttrStep{Name: "root_block_device"},
  2914  				cty.IndexStep{Key: cty.StringVal("a")},
  2915  			}),
  2916  			Tainted: false,
  2917  			Schema: &configschema.Block{
  2918  				Attributes: map[string]*configschema.Attribute{
  2919  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2920  					"ami": {Type: cty.String, Optional: true},
  2921  				},
  2922  				BlockTypes: map[string]*configschema.NestedBlock{
  2923  					"root_block_device": {
  2924  						Block: configschema.Block{
  2925  							Attributes: map[string]*configschema.Attribute{
  2926  								"volume_type": {
  2927  									Type:     cty.String,
  2928  									Optional: true,
  2929  									Computed: true,
  2930  								},
  2931  							},
  2932  						},
  2933  						Nesting: configschema.NestingMap,
  2934  					},
  2935  				},
  2936  			},
  2937  			ExpectedOutput: `  # test_instance.example must be replaced
  2938  -/+ resource "test_instance" "example" {
  2939        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  2940          id  = "i-02ae66f368e8518a9"
  2941  
  2942        ~ root_block_device "a" { # forces replacement
  2943            ~ volume_type = "gp2" -> "different"
  2944          }
  2945          root_block_device "b" {
  2946              volume_type = "standard"
  2947          }
  2948      }
  2949  `,
  2950  		},
  2951  		"in-place update - deletion": {
  2952  			Action: plans.Update,
  2953  			Mode:   addrs.ManagedResourceMode,
  2954  			Before: cty.ObjectVal(map[string]cty.Value{
  2955  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2956  				"ami": cty.StringVal("ami-BEFORE"),
  2957  				"root_block_device": cty.MapVal(map[string]cty.Value{
  2958  					"a": cty.ObjectVal(map[string]cty.Value{
  2959  						"volume_type": cty.StringVal("gp2"),
  2960  						"new_field":   cty.StringVal("new_value"),
  2961  					}),
  2962  				}),
  2963  			}),
  2964  			After: cty.ObjectVal(map[string]cty.Value{
  2965  				"id":  cty.StringVal("i-02ae66f368e8518a9"),
  2966  				"ami": cty.StringVal("ami-AFTER"),
  2967  				"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
  2968  					"volume_type": cty.String,
  2969  					"new_field":   cty.String,
  2970  				})),
  2971  			}),
  2972  			RequiredReplace: cty.NewPathSet(),
  2973  			Tainted:         false,
  2974  			Schema: &configschema.Block{
  2975  				Attributes: map[string]*configschema.Attribute{
  2976  					"id":  {Type: cty.String, Optional: true, Computed: true},
  2977  					"ami": {Type: cty.String, Optional: true},
  2978  				},
  2979  				BlockTypes: map[string]*configschema.NestedBlock{
  2980  					"root_block_device": {
  2981  						Block: configschema.Block{
  2982  							Attributes: map[string]*configschema.Attribute{
  2983  								"volume_type": {
  2984  									Type:     cty.String,
  2985  									Optional: true,
  2986  									Computed: true,
  2987  								},
  2988  								"new_field": {
  2989  									Type:     cty.String,
  2990  									Optional: true,
  2991  									Computed: true,
  2992  								},
  2993  							},
  2994  						},
  2995  						Nesting: configschema.NestingMap,
  2996  					},
  2997  				},
  2998  			},
  2999  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3000    ~ resource "test_instance" "example" {
  3001        ~ ami = "ami-BEFORE" -> "ami-AFTER"
  3002          id  = "i-02ae66f368e8518a9"
  3003  
  3004        - root_block_device "a" {
  3005            - new_field   = "new_value" -> null
  3006            - volume_type = "gp2" -> null
  3007          }
  3008      }
  3009  `,
  3010  		},
  3011  		"in-place sequence update - deletion": {
  3012  			Action: plans.Update,
  3013  			Mode:   addrs.ManagedResourceMode,
  3014  			Before: cty.ObjectVal(map[string]cty.Value{
  3015  				"list": cty.ListVal([]cty.Value{
  3016  					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("x")}),
  3017  					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}),
  3018  				}),
  3019  			}),
  3020  			After: cty.ObjectVal(map[string]cty.Value{
  3021  				"list": cty.ListVal([]cty.Value{
  3022  					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}),
  3023  					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("z")}),
  3024  				}),
  3025  			}),
  3026  			RequiredReplace: cty.NewPathSet(),
  3027  			Tainted:         false,
  3028  			Schema: &configschema.Block{
  3029  				BlockTypes: map[string]*configschema.NestedBlock{
  3030  					"list": {
  3031  						Block: configschema.Block{
  3032  							Attributes: map[string]*configschema.Attribute{
  3033  								"attr": {
  3034  									Type:     cty.String,
  3035  									Required: true,
  3036  								},
  3037  							},
  3038  						},
  3039  						Nesting: configschema.NestingList,
  3040  					},
  3041  				},
  3042  			},
  3043  			ExpectedOutput: `  # test_instance.example will be updated in-place
  3044    ~ resource "test_instance" "example" {
  3045        ~ list {
  3046            ~ attr = "x" -> "y"
  3047          }
  3048        ~ list {
  3049            ~ attr = "y" -> "z"
  3050          }
  3051      }
  3052  `,
  3053  		},
  3054  	}
  3055  	runTestCases(t, testCases)
  3056  }
  3057  
  3058  type testCase struct {
  3059  	Action          plans.Action
  3060  	Mode            addrs.ResourceMode
  3061  	Before          cty.Value
  3062  	After           cty.Value
  3063  	Schema          *configschema.Block
  3064  	RequiredReplace cty.PathSet
  3065  	Tainted         bool
  3066  	ExpectedOutput  string
  3067  }
  3068  
  3069  func runTestCases(t *testing.T, testCases map[string]testCase) {
  3070  	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
  3071  
  3072  	for name, tc := range testCases {
  3073  		t.Run(name, func(t *testing.T) {
  3074  			ty := tc.Schema.ImpliedType()
  3075  
  3076  			beforeVal := tc.Before
  3077  			switch { // Some fixups to make the test cases a little easier to write
  3078  			case beforeVal.IsNull():
  3079  				beforeVal = cty.NullVal(ty) // allow mistyped nulls
  3080  			case !beforeVal.IsKnown():
  3081  				beforeVal = cty.UnknownVal(ty) // allow mistyped unknowns
  3082  			}
  3083  			before, err := plans.NewDynamicValue(beforeVal, ty)
  3084  			if err != nil {
  3085  				t.Fatal(err)
  3086  			}
  3087  
  3088  			afterVal := tc.After
  3089  			switch { // Some fixups to make the test cases a little easier to write
  3090  			case afterVal.IsNull():
  3091  				afterVal = cty.NullVal(ty) // allow mistyped nulls
  3092  			case !afterVal.IsKnown():
  3093  				afterVal = cty.UnknownVal(ty) // allow mistyped unknowns
  3094  			}
  3095  			after, err := plans.NewDynamicValue(afterVal, ty)
  3096  			if err != nil {
  3097  				t.Fatal(err)
  3098  			}
  3099  
  3100  			change := &plans.ResourceInstanceChangeSrc{
  3101  				Addr: addrs.Resource{
  3102  					Mode: tc.Mode,
  3103  					Type: "test_instance",
  3104  					Name: "example",
  3105  				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
  3106  				ProviderAddr: addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance),
  3107  				ChangeSrc: plans.ChangeSrc{
  3108  					Action: tc.Action,
  3109  					Before: before,
  3110  					After:  after,
  3111  				},
  3112  				RequiredReplace: tc.RequiredReplace,
  3113  			}
  3114  
  3115  			output := ResourceChange(change, tc.Tainted, tc.Schema, color)
  3116  			if output != tc.ExpectedOutput {
  3117  				t.Fatalf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.ExpectedOutput)
  3118  			}
  3119  		})
  3120  	}
  3121  }