Merge pull request #26187 from hashicorp/alisdair/concise-diff
command: Add experimental concise diff renderer
This commit is contained in:
commit
a18e1cb24f
|
@ -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.`
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue