diff --git a/internal/command/format/diff.go b/internal/command/format/diff.go index b87b11c18..686b6e6f0 100644 --- a/internal/command/format/diff.go +++ b/internal/command/format/diff.go @@ -471,8 +471,28 @@ func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.At } if attrS.NestedType != nil { - p.writeNestedAttrDiff(name, attrS.NestedType, old, new, nameLen, indent, path, action, showJustNew) - return false + renderNested := true + + // If the collection values are empty or null, we render them as single attributes + switch attrS.NestedType.Nesting { + case configschema.NestingList, configschema.NestingSet, configschema.NestingMap: + var oldLen, newLen int + if !old.IsNull() && old.IsKnown() { + oldLen = old.LengthInt() + } + if !new.IsNull() && new.IsKnown() { + newLen = new.LengthInt() + } + + if oldLen+newLen == 0 { + renderNested = false + } + } + + if renderNested { + p.writeNestedAttrDiff(name, attrS.NestedType, old, new, nameLen, indent, path, action, showJustNew) + return false + } } p.buf.WriteString("\n") @@ -613,6 +633,7 @@ func (p *blockBodyDiffPrinter) writeNestedAttrDiff( allItems := make([]cty.Value, 0, len(oldItems)+len(newItems)) allItems = append(allItems, oldItems...) allItems = append(allItems, newItems...) + all := cty.SetVal(allItems) p.buf.WriteString(" = [") @@ -625,11 +646,11 @@ func (p *blockBodyDiffPrinter) writeNestedAttrDiff( case !val.IsKnown(): action = plans.Update newValue = val - case !old.HasElement(val).True(): + case old.IsNull() || !old.HasElement(val).True(): action = plans.Create oldValue = cty.NullVal(val.Type()) newValue = val - case !new.HasElement(val).True(): + case new.IsNull() || !new.HasElement(val).True(): action = plans.Delete oldValue = val newValue = cty.NullVal(val.Type()) diff --git a/internal/command/format/diff_test.go b/internal/command/format/diff_test.go index 2b4ece8d2..fa6558f89 100644 --- a/internal/command/format/diff_test.go +++ b/internal/command/format/diff_test.go @@ -2861,6 +2861,97 @@ func TestResourceChange_nestedSet(t *testing.T) { - volume_type = "gp2" -> null } } +`, + }, + "in-place update - empty nested sets": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + }))), + "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + })), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + })), + "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + })), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchema(configschema.NestingSet), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + + disks = [] + id = "i-02ae66f368e8518a9" + } +`, + }, + "in-place update - null insertion": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + }))), + "root_block_device": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + "new_field": cty.NullVal(cty.String), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + "new_field": cty.StringVal("new_value"), + }), + }), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaPlus(configschema.NestingSet), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + + disks = [ + + { + + mount_point = "/var/diska" + + size = "50GB" + }, + ] + id = "i-02ae66f368e8518a9" + + + root_block_device { + + new_field = "new_value" + + volume_type = "gp2" + } + - root_block_device { + - volume_type = "gp2" -> null + } + } `, }, }