command: Add experimental concise diff renderer
When rendering a diff between current state and projected state, we only show resources and outputs which have changes. However, we show a full structural diff for these values, which includes all attributes and blocks for a changed resource or output. The result can be a very long diff, which makes it difficult to verify what the changed fields are. This commit adds an experimental concise diff renderer, which suppresses most unchanged fields, only displaying the most relevant changes and some identifying context. This means: - Always show all identifying attributes, initially defined as `id`, `name`, and `tags`, even if unchanged; - Only show changed, added, or removed primitive values: `string`, `number`, or `bool`; - Only show added or removed elements in unordered collections and structural types: `map`, `set`, and `object`; - Show added or removed elements with any surrounding unchanged elements for sequence types: `list` and `tuple`; - Only show added or removed nested blocks, or blocks with changed attributes. If any attributes, collection elements, or blocks are hidden, a count is kept and displayed at the end of the parent scope. This ensures that it is clear that the diff is only displaying a subset of the resource. The experiment is currently enabled by default, but can be disabled by setting the TF_X_CONCISE_DIFF environment variable to 0.
This commit is contained in:
parent
ee544aa4e8
commit
09d8355f43
|
@ -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