diff --git a/command/format/diff.go b/command/format/diff.go index d2b84571d..23b8d095f 100644 --- a/command/format/diff.go +++ b/command/format/diff.go @@ -219,6 +219,7 @@ func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, ol // write the attributes diff blankBeforeBlocks := p.writeAttrsDiff(schema.Attributes, old, new, indent, path, &result) + p.writeSkippedAttr(result.skippedAttributes, indent+2) { blockTypeNames := make([]string, 0, len(schema.BlockTypes)) @@ -299,16 +300,6 @@ func (p *blockBodyDiffPrinter) writeAttrsDiff( } } - if result.skippedAttributes > 0 { - noun := "attributes" - if result.skippedAttributes == 1 { - noun = "attribute" - } - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", result.skippedAttributes, noun))) - } - return blankBeforeBlocks } @@ -405,17 +396,7 @@ func (p *blockBodyDiffPrinter) writeNestedAttrDiff( p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } p.writeAttrsDiff(objS.Attributes, old, new, indent+2, path, result) - - if result.skippedAttributes > 0 { - noun := "attributes" - if result.skippedAttributes == 1 { - noun = "attribute" - } - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", result.skippedAttributes, noun))) - } - + p.writeSkippedAttr(result.skippedAttributes, indent+4) p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString("}") @@ -444,6 +425,9 @@ func (p *blockBodyDiffPrinter) writeNestedAttrDiff( // of the lists will be presented as either creates (+) or deletes (-) // depending on which list they belong to. var commonLen int + // unchanged is the number of unchanged elements + var unchanged int + switch { case len(oldItems) < len(newItems): commonLen = len(oldItems) @@ -456,25 +440,31 @@ func (p *blockBodyDiffPrinter) writeNestedAttrDiff( newItem := newItems[i] if oldItem.RawEquals(newItem) { action = plans.NoOp + unchanged++ + } + if action != plans.NoOp { + p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+6, path, result) + p.writeSkippedAttr(result.skippedAttributes, indent+8) + p.buf.WriteString("\n") } - p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+6, path, result) } for i := commonLen; i < len(oldItems); i++ { path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) oldItem := oldItems[i] newItem := cty.NullVal(oldItem.Type()) p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+6, path, result) + p.buf.WriteString("\n") } for i := commonLen; i < len(newItems); i++ { path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) newItem := newItems[i] oldItem := cty.NullVal(newItem.Type()) p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+6, path, result) + p.buf.WriteString("\n") } - - p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent+4)) p.buf.WriteString("},\n") + p.writeSkippedElems(unchanged, indent+4) p.buf.WriteString(strings.Repeat(" ", indent+2)) p.buf.WriteString("]") @@ -548,8 +538,10 @@ func (p *blockBodyDiffPrinter) writeNestedAttrDiff( } sort.Strings(allKeysOrder) - p.buf.WriteString(" = {") + p.buf.WriteString(" = {\n") + // unchanged tracks the number of unchanged elements + unchanged := 0 for _, k := range allKeysOrder { var action plans.Action oldValue := oldItems[k] @@ -565,26 +557,27 @@ func (p *blockBodyDiffPrinter) writeNestedAttrDiff( action = plans.Update default: action = plans.NoOp + unchanged++ } - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent+4)) - p.writeActionSymbol(action) + if action != plans.NoOp { + p.buf.WriteString(strings.Repeat(" ", indent+4)) + p.writeActionSymbol(action) + fmt.Fprintf(p.buf, "%q = {", k) + if p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1]) { + p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) + } - fmt.Fprintf(p.buf, "%q = {", k) - if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) + path := append(path, cty.IndexStep{Key: cty.StringVal(k)}) + p.writeAttrsDiff(objS.Attributes, oldValue, newValue, indent+6, path, result) + p.writeSkippedAttr(result.skippedAttributes, indent+8) + p.buf.WriteString("\n") + p.buf.WriteString(strings.Repeat(" ", indent+4)) + p.buf.WriteString("},\n") } - - path := append(path, cty.IndexStep{Key: cty.StringVal(k)}) - - p.writeAttrsDiff(objS.Attributes, oldValue, newValue, indent+6, path, result) - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent+4)) - p.buf.WriteString("},") } - p.buf.WriteString("\n") + p.writeSkippedElems(unchanged, indent+4) p.buf.WriteString(strings.Repeat(" ", indent+2)) p.buf.WriteString("}") } @@ -1836,3 +1829,27 @@ func DiffActionSymbol(action plans.Action) string { func identifyingAttribute(name string, attrSchema *configschema.Attribute) bool { return name == "id" || name == "tags" || name == "name" } + +func (p *blockBodyDiffPrinter) writeSkippedAttr(skipped, indent int) { + if skipped > 0 { + noun := "attributes" + if skipped == 1 { + noun = "attribute" + } + p.buf.WriteString("\n") + p.buf.WriteString(strings.Repeat(" ", indent)) + p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", skipped, noun))) + } +} + +func (p *blockBodyDiffPrinter) writeSkippedElems(skipped, indent int) { + if skipped > 0 { + noun := "elements" + if skipped == 1 { + noun = "element" + } + p.buf.WriteString(strings.Repeat(" ", indent)) + p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", skipped, noun))) + p.buf.WriteString("\n") + } +} diff --git a/command/format/diff_test.go b/command/format/diff_test.go index 63275c958..41c42553e 100644 --- a/command/format/diff_test.go +++ b/command/format/diff_test.go @@ -2309,6 +2309,10 @@ func TestResourceChange_nestedList(t *testing.T) { "mount_point": cty.StringVal("/var/diska"), "size": cty.NullVal(cty.String), }), + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diskb"), + "size": cty.StringVal("50GB"), + }), }), "root_block_device": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ @@ -2325,6 +2329,10 @@ func TestResourceChange_nestedList(t *testing.T) { "mount_point": cty.StringVal("/var/diska"), "size": cty.StringVal("50GB"), }), + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diskb"), + "size": cty.StringVal("50GB"), + }), }), "root_block_device": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ @@ -2343,6 +2351,7 @@ func TestResourceChange_nestedList(t *testing.T) { + size = "50GB" # (1 unchanged attribute hidden) }, + # (1 unchanged element hidden) ] id = "i-02ae66f368e8518a9" @@ -2992,9 +3001,7 @@ func TestResourceChange_nestedMap(t *testing.T) { + mount_point = "/var/disk2" + size = "50GB" }, - "disk_a" = { - # (2 unchanged attributes hidden) - }, + # (1 unchanged element hidden) } id = "i-02ae66f368e8518a9" @@ -4019,6 +4026,51 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { ~ ami = (sensitive value) # forces replacement id = "i-02ae66f368e8518a9" } +`, + }, + "update with sensitive nested type attribute forcing replacement": { + Action: plans.DeleteThenCreate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "conn_info": cty.ObjectVal(map[string]cty.Value{ + "user": cty.StringVal("not-secret"), + "password": cty.StringVal("top-secret"), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "conn_info": cty.ObjectVal(map[string]cty.Value{ + "user": cty.StringVal("not-secret"), + "password": cty.StringVal("new-secret"), + }), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "conn_info": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "user": {Type: cty.String, Optional: true}, + "password": {Type: cty.String, Optional: true, Sensitive: true}, + }, + }, + }, + }, + }, + RequiredReplace: cty.NewPathSet( + cty.GetAttrPath("conn_info"), + cty.GetAttrPath("password"), + ), + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ conn_info = { # forces replacement + ~ password = (sensitive value) + # (1 unchanged attribute hidden) + } + id = "i-02ae66f368e8518a9" + } `, }, }