diff --git a/command/format/diff.go b/command/format/diff.go index a3e7657a1..d816e17f1 100644 --- a/command/format/diff.go +++ b/command/format/diff.go @@ -100,7 +100,7 @@ func ResourceChange( buf.WriteString(addr.String()) } - buf.WriteString(" {\n") + buf.WriteString(" {") p := blockBodyDiffPrinter{ buf: &buf, @@ -122,9 +122,12 @@ func ResourceChange( panic(fmt.Sprintf("failed to decode plan for %s while rendering diff: %s", addr, err)) } - p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path) - - buf.WriteString(" }\n") + bodyWritten := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path) + if bodyWritten { + buf.WriteString("\n") + buf.WriteString(strings.Repeat(" ", 4)) + } + buf.WriteString("}\n") return buf.String() } @@ -143,9 +146,12 @@ type blockBodyDiffPrinter struct { const forcesNewResourceCaption = " [red]# forces replacement[reset]" -func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, old, new cty.Value, indent int, path cty.Path) { +// writeBlockBodyDiff writes attribute or block differences +// and returns true if any differences were found and written +func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, old, new cty.Value, indent int, path cty.Path) bool { path = ctyEnsurePathCapacity(path, 1) + bodyWritten := false blankBeforeBlocks := false { attrNames := make([]string, 0, len(schema.Attributes)) @@ -176,6 +182,7 @@ func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, ol oldVal := ctyGetAttrMaybeNull(old, name) newVal := ctyGetAttrMaybeNull(new, name) + bodyWritten = true p.writeAttrDiff(name, attrS, oldVal, newVal, attrNameLen, indent, path) } } @@ -192,16 +199,20 @@ func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, ol oldVal := ctyGetAttrMaybeNull(old, name) newVal := ctyGetAttrMaybeNull(new, name) + bodyWritten = true p.writeNestedBlockDiffs(name, blockS, oldVal, newVal, blankBeforeBlocks, indent, path) // Always include a blank for any subsequent block types. blankBeforeBlocks = true } } + + return bodyWritten } func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) { path = append(path, cty.GetAttrStep{Name: name}) + p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent)) showJustNew := false var action plans.Action @@ -239,9 +250,6 @@ func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.At p.writeValueDiff(old, new, indent+2, path) } } - - p.buf.WriteString("\n") - } func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *configschema.NestedBlock, old, new cty.Value, blankBefore bool, indent int, path cty.Path) { @@ -393,6 +401,7 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *config } func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string, blockS *configschema.Block, action plans.Action, old, new cty.Value, indent int, path cty.Path) { + p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent)) p.writeActionSymbol(action) @@ -406,12 +415,12 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string, p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } - p.buf.WriteString("\n") - - p.writeBlockBodyDiff(blockS, old, new, indent+4, path) - - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.buf.WriteString("}\n") + bodyWritten := p.writeBlockBodyDiff(blockS, old, new, indent+4, path) + if bodyWritten { + p.buf.WriteString("\n") + p.buf.WriteString(strings.Repeat(" ", indent+2)) + } + p.buf.WriteString("}") } func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) { diff --git a/command/format/diff_test.go b/command/format/diff_test.go index 4e1d98481..5fc4637dc 100644 --- a/command/format/diff_test.go +++ b/command/format/diff_test.go @@ -1282,6 +1282,53 @@ func TestResourceChange_nestedList(t *testing.T) { `, }, "in-place update - creation": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "root_block_device": cty.ListValEmpty(cty.EmptyObject), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "root_block_device": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.NullVal(cty.String), + }), + }), + }), + RequiredReplace: cty.NewPathSet(), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "root_block_device": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "volume_type": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + Nesting: configschema.NestingList, + }, + }, + }, + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + + + root_block_device {} + } +`, + }, + "in-place update - first insertion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{