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