diff --git a/backend/local/backend_plan_test.go b/backend/local/backend_plan_test.go index ca40ca6358..64025edec7 100644 --- a/backend/local/backend_plan_test.go +++ b/backend/local/backend_plan_test.go @@ -289,12 +289,9 @@ Terraform will perform the following actions: # test_instance.foo is tainted, so must be replaced -/+ resource "test_instance" "foo" { - ami = "bar" + # (1 unchanged attribute hidden) - network_interface { - description = "Main network interface" - device_index = 0 - } + # (1 unchanged block hidden) } Plan: 1 to add, 0 to change, 1 to destroy.` @@ -468,12 +465,9 @@ Terraform will perform the following actions: # test_instance.foo is tainted, so must be replaced +/- resource "test_instance" "foo" { - ami = "bar" + # (1 unchanged attribute hidden) - network_interface { - description = "Main network interface" - device_index = 0 - } + # (1 unchanged block hidden) } Plan: 1 to add, 0 to change, 1 to destroy.` diff --git a/command/format/diff.go b/command/format/diff.go index 2e4208ff07..901c9a081f 100644 --- a/command/format/diff.go +++ b/command/format/diff.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/helper/experiment" "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/plans/objchange" "github.com/hashicorp/terraform/states" @@ -98,6 +99,7 @@ func ResourceChange( color: color, action: change.Action, requiredReplace: change.RequiredReplace, + concise: experiment.Enabled(experiment.X_concise_diff), } // Most commonly-used resources have nested blocks that result in us @@ -123,10 +125,10 @@ func ResourceChange( changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, schema) changeV.Change.After = objchange.NormalizeObjectFromLegacySDK(changeV.Change.After, schema) - bodyWritten := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path) - if bodyWritten { - buf.WriteString("\n") - buf.WriteString(strings.Repeat(" ", 4)) + result := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path) + if result.bodyWritten { + p.buf.WriteString("\n") + p.buf.WriteString(strings.Repeat(" ", 4)) } buf.WriteString("}\n") @@ -144,9 +146,10 @@ func OutputChanges( ) string { var buf bytes.Buffer p := blockBodyDiffPrinter{ - buf: &buf, - color: color, - action: plans.Update, // not actually used in this case, because we're not printing a containing block + buf: &buf, + color: color, + action: plans.Update, // not actually used in this case, because we're not printing a containing block + concise: experiment.Enabled(experiment.X_concise_diff), } // We're going to reuse the codepath we used for printing resource block @@ -189,16 +192,24 @@ type blockBodyDiffPrinter struct { color *colorstring.Colorize action plans.Action requiredReplace cty.PathSet + concise bool +} + +type blockBodyDiffResult struct { + bodyWritten bool + skippedAttributes int + skippedBlocks int } const forcesNewResourceCaption = " [red]# forces replacement[reset]" // 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 { +func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, old, new cty.Value, indent int, path cty.Path) blockBodyDiffResult { path = ctyEnsurePathCapacity(path, 1) - bodyWritten := false + result := blockBodyDiffResult{} + blankBeforeBlocks := false { attrNames := make([]string, 0, len(schema.Attributes)) @@ -229,8 +240,21 @@ 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) + result.bodyWritten = true + skipped := p.writeAttrDiff(name, attrS, oldVal, newVal, attrNameLen, indent, path) + if skipped { + result.skippedAttributes++ + } + } + + 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))) } } @@ -246,21 +270,31 @@ 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) + result.bodyWritten = true + skippedBlocks := p.writeNestedBlockDiffs(name, blockS, oldVal, newVal, blankBeforeBlocks, indent, path) + if skippedBlocks > 0 { + result.skippedBlocks += skippedBlocks + } // Always include a blank for any subsequent block types. blankBeforeBlocks = true } + if result.skippedBlocks > 0 { + noun := "blocks" + if result.skippedBlocks == 1 { + noun = "block" + } + 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.skippedBlocks, noun))) + } } - return bodyWritten + return result } -func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) { +func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) bool { path = append(path, cty.GetAttrStep{Name: name}) - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent)) showJustNew := false var action plans.Action switch { @@ -276,6 +310,12 @@ func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.At action = plans.Update } + if action == plans.NoOp && p.concise && !identifyingAttribute(name, attrS) { + return true + } + + p.buf.WriteString("\n") + p.buf.WriteString(strings.Repeat(" ", indent)) p.writeActionSymbol(action) p.buf.WriteString(p.color.Color("[bold]")) @@ -300,13 +340,16 @@ func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.At p.writeValueDiff(old, new, indent+2, path) } } + + return false } -func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *configschema.NestedBlock, old, new cty.Value, blankBefore bool, indent int, path cty.Path) { +func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *configschema.NestedBlock, old, new cty.Value, blankBefore bool, indent int, path cty.Path) int { + skippedBlocks := 0 path = append(path, cty.GetAttrStep{Name: name}) if old.IsNull() && new.IsNull() { // Nothing to do if both old and new is null - return + return skippedBlocks } // Where old/new are collections representing a nesting mode other than @@ -335,7 +378,10 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *config if blankBefore { p.buf.WriteRune('\n') } - p.writeNestedBlockDiff(name, nil, &blockS.Block, action, old, new, indent, path) + skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, old, new, indent, path) + if skipped { + return 1 + } case configschema.NestingList: // For the sake of handling nested blocks, we'll treat a null list // the same as an empty list since the config language doesn't @@ -377,19 +423,28 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *config if oldItem.RawEquals(newItem) { action = plans.NoOp } - p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldItem, newItem, indent, path) + skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldItem, newItem, indent, path) + if skipped { + skippedBlocks++ + } } 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.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Delete, oldItem, newItem, indent, path) + skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Delete, oldItem, newItem, indent, path) + if skipped { + skippedBlocks++ + } } 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.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Create, oldItem, newItem, indent, path) + skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Create, oldItem, newItem, indent, path) + if skipped { + skippedBlocks++ + } } case configschema.NestingSet: // For the sake of handling nested blocks, we'll treat a null set @@ -403,7 +458,7 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *config if (len(oldItems) + len(newItems)) == 0 { // Nothing to do if both sets are empty - return + return 0 } allItems := make([]cty.Value, 0, len(oldItems)+len(newItems)) @@ -437,7 +492,10 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *config newValue = val } path := append(path, cty.IndexStep{Key: val}) - p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldValue, newValue, indent, path) + skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldValue, newValue, indent, path) + if skipped { + skippedBlocks++ + } } case configschema.NestingMap: @@ -451,7 +509,7 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *config newItems := new.AsValueMap() if (len(oldItems) + len(newItems)) == 0 { // Nothing to do if both maps are empty - return + return 0 } allKeys := make(map[string]bool) @@ -489,12 +547,20 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *config } path := append(path, cty.IndexStep{Key: cty.StringVal(k)}) - p.writeNestedBlockDiff(name, &k, &blockS.Block, action, oldValue, newValue, indent, path) + skipped := p.writeNestedBlockDiff(name, &k, &blockS.Block, action, oldValue, newValue, indent, path) + if skipped { + skippedBlocks++ + } } } + return skippedBlocks } -func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string, blockS *configschema.Block, action plans.Action, old, new cty.Value, indent int, path cty.Path) { +func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string, blockS *configschema.Block, action plans.Action, old, new cty.Value, indent int, path cty.Path) bool { + if action == plans.NoOp && p.concise { + return true + } + p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent)) p.writeActionSymbol(action) @@ -509,12 +575,14 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string, p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } - bodyWritten := p.writeBlockBodyDiff(blockS, old, new, indent+4, path) - if bodyWritten { + result := p.writeBlockBodyDiff(blockS, old, new, indent+4, path) + if result.bodyWritten { p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent+2)) } p.buf.WriteString("}") + + return false } func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) { @@ -819,11 +887,10 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa removed = cty.SetValEmpty(ty.ElementType()) } + suppressedElements := 0 for it := all.ElementIterator(); it.Next(); { _, val := it.Element() - p.buf.WriteString(strings.Repeat(" ", indent+2)) - var action plans.Action switch { case !val.IsKnown(): @@ -836,11 +903,28 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa action = plans.NoOp } + if action == plans.NoOp && p.concise { + suppressedElements++ + continue + } + + p.buf.WriteString(strings.Repeat(" ", indent+2)) p.writeActionSymbol(action) p.writeValue(val, action, indent+4) p.buf.WriteString(",\n") } + if suppressedElements > 0 { + p.writeActionSymbol(plans.NoOp) + p.buf.WriteString(strings.Repeat(" ", indent+2)) + noun := "elements" + if suppressedElements == 1 { + noun = "element" + } + p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", suppressedElements, noun))) + p.buf.WriteString("\n") + } + p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString("]") return @@ -852,7 +936,74 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa p.buf.WriteString("\n") elemDiffs := ctySequenceDiff(old.AsValueSlice(), new.AsValueSlice()) - for _, elemDiff := range elemDiffs { + + // Maintain a stack of suppressed lines in the diff for later + // display or elision + var suppressedElements []*plans.Change + var changeShown bool + + for i := 0; i < len(elemDiffs); i++ { + // In concise mode, push any no-op diff elements onto the stack + if p.concise { + for i < len(elemDiffs) && elemDiffs[i].Action == plans.NoOp { + suppressedElements = append(suppressedElements, elemDiffs[i]) + i++ + } + } + + // If we have some suppressed elements on the stackā€¦ + if len(suppressedElements) > 0 { + // If we've just rendered a change, display the first + // element in the stack as context + if changeShown { + elemDiff := suppressedElements[0] + p.buf.WriteString(strings.Repeat(" ", indent+4)) + p.writeValue(elemDiff.After, elemDiff.Action, indent+4) + p.buf.WriteString(",\n") + suppressedElements = suppressedElements[1:] + } + + hidden := len(suppressedElements) + + // If we're not yet at the end of the list, capture the + // last element on the stack as context for the upcoming + // change to be rendered + var nextContextDiff *plans.Change + if hidden > 0 && i < len(elemDiffs) { + hidden-- + nextContextDiff = suppressedElements[hidden] + suppressedElements = suppressedElements[:hidden] + } + + // If there are still hidden elements, show an elision + // statement counting them + if hidden > 0 { + p.writeActionSymbol(plans.NoOp) + p.buf.WriteString(strings.Repeat(" ", indent+2)) + noun := "elements" + if hidden == 1 { + noun = "element" + } + p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", hidden, noun))) + p.buf.WriteString("\n") + } + + // Display the next context diff if it was captured above + if nextContextDiff != nil { + p.buf.WriteString(strings.Repeat(" ", indent+4)) + p.writeValue(nextContextDiff.After, nextContextDiff.Action, indent+4) + p.buf.WriteString(",\n") + } + + // Suppressed elements have now been handled so clear them again + suppressedElements = nil + } + + if i >= len(elemDiffs) { + break + } + + elemDiff := elemDiffs[i] p.buf.WriteString(strings.Repeat(" ", indent+2)) p.writeActionSymbol(elemDiff.Action) switch elemDiff.Action { @@ -869,10 +1020,12 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa } p.buf.WriteString(",\n") + changeShown = true } p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString("]") + return case ty.IsMapType(): @@ -903,6 +1056,7 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa sort.Strings(allKeys) + suppressedElements := 0 lastK := "" for i, k := range allKeys { if i > 0 && lastK == k { @@ -910,7 +1064,6 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa } lastK = k - p.buf.WriteString(strings.Repeat(" ", indent+2)) kV := cty.StringVal(k) var action plans.Action if old.HasIndex(kV).False() { @@ -923,8 +1076,14 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa action = plans.Update } + if action == plans.NoOp && p.concise { + suppressedElements++ + continue + } + path := append(path, cty.IndexStep{Key: kV}) + p.buf.WriteString(strings.Repeat(" ", indent+2)) p.writeActionSymbol(action) p.writeValue(kV, action, indent+4) p.buf.WriteString(strings.Repeat(" ", keyLen-len(k))) @@ -946,8 +1105,20 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa p.buf.WriteByte('\n') } + if suppressedElements > 0 { + p.writeActionSymbol(plans.NoOp) + p.buf.WriteString(strings.Repeat(" ", indent+2)) + noun := "elements" + if suppressedElements == 1 { + noun = "element" + } + p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", suppressedElements, noun))) + p.buf.WriteString("\n") + } + p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString("}") + return case ty.IsObjectType(): p.buf.WriteString("{") @@ -976,6 +1147,7 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa sort.Strings(allKeys) + suppressedElements := 0 lastK := "" for i, k := range allKeys { if i > 0 && lastK == k { @@ -983,7 +1155,6 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa } lastK = k - p.buf.WriteString(strings.Repeat(" ", indent+2)) kV := k var action plans.Action if !old.Type().HasAttribute(kV) { @@ -996,8 +1167,14 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa action = plans.Update } + if action == plans.NoOp && p.concise { + suppressedElements++ + continue + } + path := append(path, cty.GetAttrStep{Name: kV}) + p.buf.WriteString(strings.Repeat(" ", indent+2)) p.writeActionSymbol(action) p.buf.WriteString(k) p.buf.WriteString(strings.Repeat(" ", keyLen-len(k))) @@ -1020,6 +1197,17 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa p.buf.WriteString("\n") } + if suppressedElements > 0 { + p.writeActionSymbol(plans.NoOp) + p.buf.WriteString(strings.Repeat(" ", indent+2)) + noun := "elements" + if suppressedElements == 1 { + noun = "element" + } + p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", suppressedElements, noun))) + p.buf.WriteString("\n") + } + p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString("}") @@ -1266,3 +1454,11 @@ func DiffActionSymbol(action plans.Action) string { return " ?" } } + +// Extremely coarse heuristic for determining whether or not a given attribute +// name is important for identifying a resource. In the future, this may be +// replaced by a flag in the schema, but for now this is likely to be good +// enough. +func identifyingAttribute(name string, attrSchema *configschema.Attribute) bool { + return name == "id" || name == "tags" || name == "name" +} diff --git a/command/format/diff_test.go b/command/format/diff_test.go index 098786051f..00d0c82c8c 100644 --- a/command/format/diff_test.go +++ b/command/format/diff_test.go @@ -4,8 +4,10 @@ import ( "fmt" "testing" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/helper/experiment" "github.com/hashicorp/terraform/plans" "github.com/mitchellh/colorstring" "github.com/zclconf/go-cty/cty" @@ -204,12 +206,19 @@ func TestResourceChange_primitiveTypes(t *testing.T) { Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "more_lines": cty.StringVal(`original +long +multi-line +string +field `), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "more_lines": cty.StringVal(`original -new line +extremely long +multi-line +string +field `), }), Schema: &configschema.Block{ @@ -225,7 +234,11 @@ new line ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ more_lines = <<~EOT original - + new line + - long + + extremely long + multi-line + string + field EOT } `, @@ -344,6 +357,13 @@ new line RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "blah" -> (known after apply) + ~ str = "before" -> "after" + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "blah" -> (known after apply) password = (sensitive value) @@ -435,6 +455,65 @@ new line + forced = "example" # forces replacement name = "name" } +`, + }, + "show all identifying attributes even if unchanged": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "bar": cty.StringVal("bar"), + "foo": cty.StringVal("foo"), + "name": cty.StringVal("alice"), + "tags": cty.MapVal(map[string]cty.Value{ + "name": cty.StringVal("bob"), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "bar": cty.StringVal("bar"), + "foo": cty.StringVal("foo"), + "name": cty.StringVal("alice"), + "tags": cty.MapVal(map[string]cty.Value{ + "name": cty.StringVal("bob"), + }), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + "bar": {Type: cty.String, Optional: true}, + "foo": {Type: cty.String, Optional: true}, + "name": {Type: cty.String, Optional: true}, + "tags": {Type: cty.Map(cty.String), Optional: true}, + }, + }, + RequiredReplace: cty.NewPathSet(), + Tainted: false, + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + name = "alice" + tags = { + "name" = "bob" + } + # (2 unchanged attributes hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + bar = "bar" + foo = "foo" + id = "i-02ae66f368e8518a9" + name = "alice" + tags = { + "name" = "bob" + } + } `, }, } @@ -489,7 +568,7 @@ func TestResourceChange_JSON(t *testing.T) { Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`{"aaa": "value"}`), + "json_field": cty.StringVal(`{"aaa": "value","ccc": 5}`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), @@ -504,12 +583,26 @@ func TestResourceChange_JSON(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ json_field = jsonencode( + ~ { + + bbb = "new_value" + - ccc = 5 -> null + # (1 unchanged element hidden) + } + ) + } +`, + + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ { aaa = "value" + bbb = "new_value" + - ccc = 5 -> null } ) } @@ -637,6 +730,17 @@ func TestResourceChange_JSON(t *testing.T) { }), Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ json_field = jsonencode( + ~ { + + bbb = "new_value" + # (1 unchanged element hidden) + } # forces replacement + ) + } +`, + VerboseOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( @@ -757,6 +861,18 @@ func TestResourceChange_JSON(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ json_field = jsonencode( + ~ [ + # (1 unchanged element hidden) + "second", + - "third", + ] + ) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( @@ -789,6 +905,19 @@ func TestResourceChange_JSON(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ json_field = jsonencode( + ~ [ + # (1 unchanged element hidden) + "second", + + "third", + ] + ) + } +`, + + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( @@ -825,8 +954,8 @@ func TestResourceChange_JSON(t *testing.T) { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ { - first = "111" + second = "222" + # (1 unchanged element hidden) } ) } @@ -1087,6 +1216,15 @@ func TestResourceChange_primitiveList(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + + list_field = [ + + "new-element", + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1121,6 +1259,15 @@ func TestResourceChange_primitiveList(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ list_field = [ + + "new-element", + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1138,7 +1285,10 @@ func TestResourceChange_primitiveList(t *testing.T) { "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("aaaa"), - cty.StringVal("cccc"), + cty.StringVal("bbbb"), + cty.StringVal("dddd"), + cty.StringVal("eeee"), + cty.StringVal("ffff"), }), }), After: cty.ObjectVal(map[string]cty.Value{ @@ -1148,6 +1298,9 @@ func TestResourceChange_primitiveList(t *testing.T) { cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("cccc"), + cty.StringVal("dddd"), + cty.StringVal("eeee"), + cty.StringVal("ffff"), }), }), Schema: &configschema.Block{ @@ -1160,13 +1313,29 @@ func TestResourceChange_primitiveList(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ list_field = [ + # (1 unchanged element hidden) + "bbbb", + + "cccc", + "dddd", + # (2 unchanged elements hidden) + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ "aaaa", - + "bbbb", - "cccc", + "bbbb", + + "cccc", + "dddd", + "eeee", + "ffff", ] } `, @@ -1203,6 +1372,17 @@ func TestResourceChange_primitiveList(t *testing.T) { }), Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ list_field = [ # forces replacement + "aaaa", + + "bbbb", + "cccc", + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1224,6 +1404,8 @@ func TestResourceChange_primitiveList(t *testing.T) { cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("cccc"), + cty.StringVal("dddd"), + cty.StringVal("eeee"), }), }), After: cty.ObjectVal(map[string]cty.Value{ @@ -1231,6 +1413,8 @@ func TestResourceChange_primitiveList(t *testing.T) { "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("bbbb"), + cty.StringVal("dddd"), + cty.StringVal("eeee"), }), }), Schema: &configschema.Block{ @@ -1243,6 +1427,19 @@ func TestResourceChange_primitiveList(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ list_field = [ + - "aaaa", + "bbbb", + - "cccc", + "dddd", + # (1 unchanged element hidden) + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1250,6 +1447,8 @@ func TestResourceChange_primitiveList(t *testing.T) { - "aaaa", "bbbb", - "cccc", + "dddd", + "eeee", ] } `, @@ -1307,6 +1506,17 @@ func TestResourceChange_primitiveList(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ list_field = [ + - "aaaa", + - "bbbb", + - "cccc", + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1341,6 +1551,13 @@ func TestResourceChange_primitiveList(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + + list_field = [] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1379,6 +1596,18 @@ func TestResourceChange_primitiveList(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ list_field = [ + "aaaa", + - "bbbb", + + (known after apply), + "cccc", + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1401,6 +1630,8 @@ func TestResourceChange_primitiveList(t *testing.T) { cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("cccc"), + cty.StringVal("dddd"), + cty.StringVal("eeee"), }), }), After: cty.ObjectVal(map[string]cty.Value{ @@ -1411,6 +1642,8 @@ func TestResourceChange_primitiveList(t *testing.T) { cty.UnknownVal(cty.String), cty.UnknownVal(cty.String), cty.StringVal("cccc"), + cty.StringVal("dddd"), + cty.StringVal("eeee"), }), }), Schema: &configschema.Block{ @@ -1423,6 +1656,20 @@ func TestResourceChange_primitiveList(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ list_field = [ + "aaaa", + - "bbbb", + + (known after apply), + + (known after apply), + "cccc", + # (2 unchanged elements hidden) + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1432,6 +1679,72 @@ func TestResourceChange_primitiveList(t *testing.T) { + (known after apply), + (known after apply), "cccc", + "dddd", + "eeee", + ] + } +`, + }, + } + runTestCases(t, testCases) +} + +func TestResourceChange_primitiveTuple(t *testing.T) { + testCases := map[string]testCase{ + "in-place update": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "tuple_field": cty.TupleVal([]cty.Value{ + cty.StringVal("aaaa"), + cty.StringVal("bbbb"), + cty.StringVal("dddd"), + cty.StringVal("eeee"), + cty.StringVal("ffff"), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "tuple_field": cty.TupleVal([]cty.Value{ + cty.StringVal("aaaa"), + cty.StringVal("bbbb"), + cty.StringVal("cccc"), + cty.StringVal("eeee"), + cty.StringVal("ffff"), + }), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Required: true}, + "tuple_field": {Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.String, cty.String, cty.String}), Optional: true}, + }, + }, + RequiredReplace: cty.NewPathSet(), + Tainted: false, + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + id = "i-02ae66f368e8518a9" + ~ tuple_field = [ + # (1 unchanged element hidden) + "bbbb", + - "dddd", + + "cccc", + "eeee", + # (1 unchanged element hidden) + ] + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + id = "i-02ae66f368e8518a9" + ~ tuple_field = [ + "aaaa", + "bbbb", + - "dddd", + + "cccc", + "eeee", + "ffff", ] } `, @@ -1467,6 +1780,15 @@ func TestResourceChange_primitiveSet(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + + set_field = [ + + "new-element", + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1501,6 +1823,15 @@ func TestResourceChange_primitiveSet(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ set_field = [ + + "new-element", + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1540,6 +1871,16 @@ func TestResourceChange_primitiveSet(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ set_field = [ + + "bbbb", + # (2 unchanged elements hidden) + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1583,6 +1924,16 @@ func TestResourceChange_primitiveSet(t *testing.T) { }), Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ set_field = [ # forces replacement + + "bbbb", + # (2 unchanged elements hidden) + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1623,6 +1974,17 @@ func TestResourceChange_primitiveSet(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ set_field = [ + - "aaaa", + - "cccc", + # (1 unchanged element hidden) + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1685,6 +2047,16 @@ func TestResourceChange_primitiveSet(t *testing.T) { }, RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ set_field = [ + - "aaaa", + - "bbbb", + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1718,6 +2090,13 @@ func TestResourceChange_primitiveSet(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + + set_field = [] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1751,6 +2130,16 @@ func TestResourceChange_primitiveSet(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ set_field = [ + - "aaaa", + - "bbbb", + ] -> (known after apply) + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1790,6 +2179,17 @@ func TestResourceChange_primitiveSet(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ set_field = [ + - "bbbb", + ~ (known after apply), + # (1 unchanged element hidden) + ] + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1832,6 +2232,15 @@ func TestResourceChange_map(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + + map_field = { + + "new-key" = "new-element" + } + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1866,6 +2275,15 @@ func TestResourceChange_map(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ map_field = { + + "new-key" = "new-element" + } + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1905,6 +2323,16 @@ func TestResourceChange_map(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ map_field = { + + "b" = "bbbb" + # (2 unchanged elements hidden) + } + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1948,6 +2376,16 @@ func TestResourceChange_map(t *testing.T) { }), Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ map_field = { # forces replacement + + "b" = "bbbb" + # (2 unchanged elements hidden) + } + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1988,6 +2426,17 @@ func TestResourceChange_map(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ map_field = { + - "a" = "aaaa" -> null + - "c" = "cccc" -> null + # (1 unchanged element hidden) + } + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -2056,6 +2505,16 @@ func TestResourceChange_map(t *testing.T) { RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> (known after apply) + ~ map_field = { + ~ "b" = "bbbb" -> (known after apply) + # (2 unchanged elements hidden) + } + # (1 unchanged attribute hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -2121,6 +2580,14 @@ func TestResourceChange_nestedList(t *testing.T) { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" + # (1 unchanged block hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + root_block_device { volume_type = "gp2" } @@ -2284,6 +2751,17 @@ func TestResourceChange_nestedList(t *testing.T) { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" + ~ root_block_device { + + new_field = "new_value" + # (1 unchanged attribute hidden) + } + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + ~ root_block_device { + new_field = "new_value" volume_type = "gp2" @@ -2861,6 +3339,17 @@ func TestResourceChange_nestedMap(t *testing.T) { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" + ~ root_block_device "a" { + + new_field = "new_value" + # (1 unchanged attribute hidden) + } + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + ~ root_block_device "a" { + new_field = "new_value" volume_type = "gp2" @@ -2927,6 +3416,18 @@ func TestResourceChange_nestedMap(t *testing.T) { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" + + root_block_device "b" { + + new_field = "new_value" + + volume_type = "gp2" + } + # (1 unchanged block hidden) + } +`, + VerboseOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + root_block_device "a" { volume_type = "gp2" } @@ -2994,6 +3495,17 @@ func TestResourceChange_nestedMap(t *testing.T) { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" + ~ root_block_device "a" { # forces replacement + ~ volume_type = "gp2" -> "different" + } + # (1 unchanged block hidden) + } +`, + VerboseOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + ~ root_block_device "a" { # forces replacement ~ volume_type = "gp2" -> "different" } @@ -3119,6 +3631,10 @@ type testCase struct { RequiredReplace cty.PathSet Tainted bool ExpectedOutput string + + // This field and all associated values can be removed if the concise diff + // experiment succeeds. + VerboseOutput string } func runTestCases(t *testing.T, testCases map[string]testCase) { @@ -3170,9 +3686,24 @@ func runTestCases(t *testing.T, testCases map[string]testCase) { RequiredReplace: tc.RequiredReplace, } + experiment.SetEnabled(experiment.X_concise_diff, true) output := ResourceChange(change, tc.Tainted, tc.Schema, color) if output != tc.ExpectedOutput { - t.Fatalf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.ExpectedOutput) + t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.ExpectedOutput) + t.Errorf("%s", cmp.Diff(output, tc.ExpectedOutput)) + } + + // Temporary coverage for verbose diff behaviour. All lines below + // in this function can be removed if the concise diff experiment + // succeeds. + if tc.VerboseOutput == "" { + return + } + experiment.SetEnabled(experiment.X_concise_diff, false) + output = ResourceChange(change, tc.Tainted, tc.Schema, color) + if output != tc.VerboseOutput { + t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.VerboseOutput) + t.Errorf("%s", cmp.Diff(output, tc.VerboseOutput)) } }) } @@ -3243,11 +3774,11 @@ func TestOutputChanges(t *testing.T) { }, ` ~ foo = [ - "alpha", + # (1 unchanged element hidden) "beta", + "gamma", "delta", - "epsilon", + # (1 unchanged element hidden) ]`, }, "multiple outputs changed, one sensitive": { @@ -3280,6 +3811,7 @@ func TestOutputChanges(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { + experiment.SetEnabled(experiment.X_concise_diff, true) output := OutputChanges(tc.changes, color) if output != tc.output { t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.output) diff --git a/command/format/state.go b/command/format/state.go index 4c245cfb93..6d6e2cee22 100644 --- a/command/format/state.go +++ b/command/format/state.go @@ -198,8 +198,8 @@ func formatStateModule(p blockBodyDiffPrinter, m *states.Module, schemas *terraf } path := make(cty.Path, 0, 3) - bodyWritten := p.writeBlockBodyDiff(schema, val.Value, val.Value, 2, path) - if bodyWritten { + result := p.writeBlockBodyDiff(schema, val.Value, val.Value, 2, path) + if result.bodyWritten { p.buf.WriteString("\n") } diff --git a/helper/experiment/experiment.go b/helper/experiment/experiment.go index 18b8837cc5..72fdeaf9ed 100644 --- a/helper/experiment/experiment.go +++ b/helper/experiment/experiment.go @@ -53,6 +53,9 @@ var ( // Shadow graph. This is already on by default. Disabling it will be // allowed for awhile in order for it to not block operations. X_shadow = newBasicID("shadow", "SHADOW", false) + + // Concise plan diff output + X_concise_diff = newBasicID("concise_diff", "CONCISE_DIFF", true) ) // Global variables this package uses because we are a package @@ -73,6 +76,7 @@ func init() { // The list of all experiments, update this when an experiment is added. All = []ID{ X_shadow, + X_concise_diff, x_force, }