github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/command/jsonformat/plan_test.go (about)

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