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:
Alisdair McDiarmid 2020-08-19 16:47:56 -04:00
parent ee544aa4e8
commit 09d8355f43
5 changed files with 783 additions and 57 deletions

View File

@ -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.`

View File

@ -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"
}

View File

@ -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)

View File

@ -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")
}

View File

@ -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,
}