Merge pull request #20071 from hashicorp/b-fix-json-diff-formatting
command/format: Fix nested (JSON) object formatting
This commit is contained in:
commit
71d07832e2
|
@ -132,11 +132,6 @@ func ResourceChange(
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
type ctyValueDiff struct {
|
|
||||||
Action plans.Action
|
|
||||||
Value cty.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
type blockBodyDiffPrinter struct {
|
type blockBodyDiffPrinter struct {
|
||||||
buf *bytes.Buffer
|
buf *bytes.Buffer
|
||||||
color *colorstring.Colorize
|
color *colorstring.Colorize
|
||||||
|
@ -561,6 +556,7 @@ func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, in
|
||||||
|
|
||||||
func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, path cty.Path) {
|
func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, path cty.Path) {
|
||||||
ty := old.Type()
|
ty := old.Type()
|
||||||
|
typesEqual := ctyTypesEqual(ty, new.Type())
|
||||||
|
|
||||||
// We have some specialized diff implementations for certain complex
|
// We have some specialized diff implementations for certain complex
|
||||||
// values where it's useful to see a visualization of the diff of
|
// values where it's useful to see a visualization of the diff of
|
||||||
|
@ -568,10 +564,8 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa
|
||||||
// new values verbatim.
|
// new values verbatim.
|
||||||
// However, these specialized implementations can apply only if both
|
// However, these specialized implementations can apply only if both
|
||||||
// values are known and non-null.
|
// values are known and non-null.
|
||||||
if old.IsKnown() && new.IsKnown() && !old.IsNull() && !new.IsNull() {
|
if old.IsKnown() && new.IsKnown() && !old.IsNull() && !new.IsNull() && typesEqual {
|
||||||
switch {
|
switch {
|
||||||
// TODO: object diffs that behave a bit like the map diffs, including if the two object types don't exactly match
|
|
||||||
|
|
||||||
case ty == cty.String:
|
case ty == cty.String:
|
||||||
// We have special behavior for both multi-line strings in general
|
// We have special behavior for both multi-line strings in general
|
||||||
// and for strings that can parse as JSON. For the JSON handling
|
// and for strings that can parse as JSON. For the JSON handling
|
||||||
|
@ -650,31 +644,21 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa
|
||||||
|
|
||||||
diffLines := ctySequenceDiff(oldLines, newLines)
|
diffLines := ctySequenceDiff(oldLines, newLines)
|
||||||
for _, diffLine := range diffLines {
|
for _, diffLine := range diffLines {
|
||||||
line := diffLine.Value.AsString()
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
||||||
|
p.writeActionSymbol(diffLine.Action)
|
||||||
|
|
||||||
switch diffLine.Action {
|
switch diffLine.Action {
|
||||||
|
case plans.NoOp, plans.Delete:
|
||||||
|
p.buf.WriteString(diffLine.Before.AsString())
|
||||||
case plans.Create:
|
case plans.Create:
|
||||||
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
p.buf.WriteString(diffLine.After.AsString())
|
||||||
p.buf.WriteString(p.color.Color("[green]+[reset] "))
|
|
||||||
p.buf.WriteString(line)
|
|
||||||
p.buf.WriteString("\n")
|
|
||||||
case plans.Delete:
|
|
||||||
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
||||||
p.buf.WriteString(p.color.Color("[red]-[reset] "))
|
|
||||||
p.buf.WriteString(line)
|
|
||||||
p.buf.WriteString("\n")
|
|
||||||
case plans.NoOp:
|
|
||||||
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
||||||
p.buf.WriteString(p.color.Color(" "))
|
|
||||||
p.buf.WriteString(line)
|
|
||||||
p.buf.WriteString("\n")
|
|
||||||
default:
|
default:
|
||||||
// Should never happen since the above covers all
|
// Should never happen since the above covers all
|
||||||
// actions that ctySequenceDiff can return.
|
// actions that ctySequenceDiff can return for strings
|
||||||
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
p.buf.WriteString(diffLine.After.AsString())
|
||||||
p.buf.WriteString(p.color.Color("? "))
|
|
||||||
p.buf.WriteString(line)
|
|
||||||
p.buf.WriteString("\n")
|
|
||||||
}
|
}
|
||||||
|
p.buf.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
p.buf.WriteString(strings.Repeat(" ", indent)) // +4 here because there's no symbol
|
p.buf.WriteString(strings.Repeat(" ", indent)) // +4 here because there's no symbol
|
||||||
|
@ -747,7 +731,6 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa
|
||||||
p.buf.WriteString(strings.Repeat(" ", indent))
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
||||||
p.buf.WriteString("]")
|
p.buf.WriteString("]")
|
||||||
return
|
return
|
||||||
|
|
||||||
case ty.IsListType() || ty.IsTupleType():
|
case ty.IsListType() || ty.IsTupleType():
|
||||||
p.buf.WriteString("[")
|
p.buf.WriteString("[")
|
||||||
if p.pathForcesNewResource(path) {
|
if p.pathForcesNewResource(path) {
|
||||||
|
@ -759,7 +742,19 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa
|
||||||
for _, elemDiff := range elemDiffs {
|
for _, elemDiff := range elemDiffs {
|
||||||
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
||||||
p.writeActionSymbol(elemDiff.Action)
|
p.writeActionSymbol(elemDiff.Action)
|
||||||
p.writeValue(elemDiff.Value, elemDiff.Action, indent+4)
|
switch elemDiff.Action {
|
||||||
|
case plans.NoOp, plans.Delete:
|
||||||
|
p.writeValue(elemDiff.Before, elemDiff.Action, indent+4)
|
||||||
|
case plans.Update:
|
||||||
|
p.writeValueDiff(elemDiff.Before, elemDiff.After, indent+4, path)
|
||||||
|
case plans.Create:
|
||||||
|
p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
|
||||||
|
default:
|
||||||
|
// Should never happen since the above covers all
|
||||||
|
// actions that ctySequenceDiff can return.
|
||||||
|
p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
|
||||||
|
}
|
||||||
|
|
||||||
p.buf.WriteString(",\n")
|
p.buf.WriteString(",\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -841,6 +836,84 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa
|
||||||
p.buf.WriteString(strings.Repeat(" ", indent))
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
||||||
p.buf.WriteString("}")
|
p.buf.WriteString("}")
|
||||||
return
|
return
|
||||||
|
case ty.IsObjectType():
|
||||||
|
p.buf.WriteString("{")
|
||||||
|
p.buf.WriteString("\n")
|
||||||
|
|
||||||
|
forcesNewResource := p.pathForcesNewResource(path)
|
||||||
|
|
||||||
|
var allKeys []string
|
||||||
|
keyLen := 0
|
||||||
|
for it := old.ElementIterator(); it.Next(); {
|
||||||
|
k, _ := it.Element()
|
||||||
|
keyStr := k.AsString()
|
||||||
|
allKeys = append(allKeys, keyStr)
|
||||||
|
if len(keyStr) > keyLen {
|
||||||
|
keyLen = len(keyStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for it := new.ElementIterator(); it.Next(); {
|
||||||
|
k, _ := it.Element()
|
||||||
|
keyStr := k.AsString()
|
||||||
|
allKeys = append(allKeys, keyStr)
|
||||||
|
if len(keyStr) > keyLen {
|
||||||
|
keyLen = len(keyStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(allKeys)
|
||||||
|
|
||||||
|
lastK := ""
|
||||||
|
for i, k := range allKeys {
|
||||||
|
if i > 0 && lastK == k {
|
||||||
|
continue // skip duplicates (list is sorted)
|
||||||
|
}
|
||||||
|
lastK = k
|
||||||
|
|
||||||
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
||||||
|
kV := k
|
||||||
|
var action plans.Action
|
||||||
|
if !old.Type().HasAttribute(kV) {
|
||||||
|
action = plans.Create
|
||||||
|
} else if !new.Type().HasAttribute(kV) {
|
||||||
|
action = plans.Delete
|
||||||
|
} else if eqV := old.GetAttr(kV).Equals(new.GetAttr(kV)); eqV.IsKnown() && eqV.True() {
|
||||||
|
action = plans.NoOp
|
||||||
|
} else {
|
||||||
|
action = plans.Update
|
||||||
|
}
|
||||||
|
|
||||||
|
path := append(path, cty.GetAttrStep{Name: kV})
|
||||||
|
|
||||||
|
p.writeActionSymbol(action)
|
||||||
|
p.buf.WriteString(k)
|
||||||
|
p.buf.WriteString(strings.Repeat(" ", keyLen-len(k)))
|
||||||
|
p.buf.WriteString(" = ")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case plans.Create, plans.NoOp:
|
||||||
|
v := new.GetAttr(kV)
|
||||||
|
p.writeValue(v, action, indent+4)
|
||||||
|
case plans.Delete:
|
||||||
|
oldV := old.GetAttr(kV)
|
||||||
|
newV := cty.NullVal(oldV.Type())
|
||||||
|
p.writeValueDiff(oldV, newV, indent+4, path)
|
||||||
|
default:
|
||||||
|
oldV := old.GetAttr(kV)
|
||||||
|
newV := new.GetAttr(kV)
|
||||||
|
p.writeValueDiff(oldV, newV, indent+4, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
||||||
|
p.buf.WriteString("}")
|
||||||
|
|
||||||
|
if forcesNewResource {
|
||||||
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -851,6 +924,7 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa
|
||||||
} else {
|
} else {
|
||||||
p.buf.WriteString(p.color.Color(" [yellow]->[reset] "))
|
p.buf.WriteString(p.color.Color(" [yellow]->[reset] "))
|
||||||
}
|
}
|
||||||
|
|
||||||
p.writeValue(new, plans.Create, indent)
|
p.writeValue(new, plans.Create, indent)
|
||||||
if p.pathForcesNewResource(path) {
|
if p.pathForcesNewResource(path) {
|
||||||
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
||||||
|
@ -928,68 +1002,24 @@ func ctyCollectionValues(val cty.Value) []cty.Value {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func ctySequenceDiff(old, new []cty.Value) []ctyValueDiff {
|
// ctySequenceDiff returns differences between given sequences of cty.Value(s)
|
||||||
var ret []ctyValueDiff
|
// in the form of Create, Delete, or Update actions (for objects).
|
||||||
lcs := objchange.LongestCommonSubsequence(old, new)
|
func ctySequenceDiff(old, new []cty.Value) []*plans.Change {
|
||||||
var oldI, newI, lcsI int
|
|
||||||
for oldI < len(old) || newI < len(new) || lcsI < len(lcs) {
|
|
||||||
for oldI < len(old) && (lcsI >= len(lcs) || !old[oldI].RawEquals(lcs[lcsI])) {
|
|
||||||
ret = append(ret, ctyValueDiff{
|
|
||||||
Action: plans.Delete,
|
|
||||||
Value: old[oldI],
|
|
||||||
})
|
|
||||||
oldI++
|
|
||||||
}
|
|
||||||
for newI < len(new) && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI])) {
|
|
||||||
ret = append(ret, ctyValueDiff{
|
|
||||||
Action: plans.Create,
|
|
||||||
Value: new[newI],
|
|
||||||
})
|
|
||||||
newI++
|
|
||||||
}
|
|
||||||
if lcsI < len(lcs) {
|
|
||||||
ret = append(ret, ctyValueDiff{
|
|
||||||
Action: plans.NoOp,
|
|
||||||
Value: new[newI],
|
|
||||||
})
|
|
||||||
|
|
||||||
// All of our indexes advance together now, since the line
|
|
||||||
// is common to all three sequences.
|
|
||||||
lcsI++
|
|
||||||
oldI++
|
|
||||||
newI++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// ctyObjectSequenceDiff is a variant of ctySequenceDiff that only works for
|
|
||||||
// values of object types. Whereas ctySequenceDiff can only return Create
|
|
||||||
// and Delete actions, this function can additionally return Update actions
|
|
||||||
// heuristically based on similarity of objects in the lists, which must
|
|
||||||
// be greater than or equal to the caller-specified threshold.
|
|
||||||
//
|
|
||||||
// See ctyObjectSimilarity for details on what "similarity" means here.
|
|
||||||
func ctyObjectSequenceDiff(old, new []cty.Value, threshold float64) []*plans.Change {
|
|
||||||
var ret []*plans.Change
|
var ret []*plans.Change
|
||||||
lcs := objchange.LongestCommonSubsequence(old, new)
|
lcs := objchange.LongestCommonSubsequence(old, new)
|
||||||
var oldI, newI, lcsI int
|
var oldI, newI, lcsI int
|
||||||
for oldI < len(old) || newI < len(new) || lcsI < len(lcs) {
|
for oldI < len(old) || newI < len(new) || lcsI < len(lcs) {
|
||||||
for oldI < len(old) && (lcsI >= len(lcs) || !old[oldI].RawEquals(lcs[lcsI])) {
|
for oldI < len(old) && (lcsI >= len(lcs) || !old[oldI].RawEquals(lcs[lcsI])) {
|
||||||
if newI < len(new) {
|
isObjectDiff := old[oldI].Type().IsObjectType() && new[newI].Type().IsObjectType()
|
||||||
// See if the next "new" is similar enough to our "old" that
|
if isObjectDiff && newI < len(new) {
|
||||||
// we'll treat this as an Update rather than a Delete/Create.
|
ret = append(ret, &plans.Change{
|
||||||
similarity := ctyObjectSimilarity(old[oldI], new[newI])
|
Action: plans.Update,
|
||||||
if similarity >= threshold {
|
Before: old[oldI],
|
||||||
ret = append(ret, &plans.Change{
|
After: new[newI],
|
||||||
Action: plans.Update,
|
})
|
||||||
Before: old[oldI],
|
oldI++
|
||||||
After: new[newI],
|
newI++ // we also consume the next "new" in this case
|
||||||
})
|
continue
|
||||||
oldI++
|
|
||||||
newI++ // we also consume the next "new" in this case
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = append(ret, &plans.Change{
|
ret = append(ret, &plans.Change{
|
||||||
|
@ -1024,48 +1054,6 @@ func ctyObjectSequenceDiff(old, new []cty.Value, threshold float64) []*plans.Cha
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// ctyObjectSimilarity returns a number between 0 and 1 that describes
|
|
||||||
// approximately how similar the two given values are, comparing in terms of
|
|
||||||
// how many of the corresponding attributes have the same value in both
|
|
||||||
// objects.
|
|
||||||
//
|
|
||||||
// This function expects the two values to have a similar set of attribute
|
|
||||||
// names, though doesn't mind if the two slightly differ since it will
|
|
||||||
// count missing attributes as differences.
|
|
||||||
//
|
|
||||||
// This function will panic if either of the given values is not an object.
|
|
||||||
func ctyObjectSimilarity(old, new cty.Value) float64 {
|
|
||||||
oldType := old.Type()
|
|
||||||
newType := new.Type()
|
|
||||||
attrNames := make(map[string]struct{})
|
|
||||||
for name := range oldType.AttributeTypes() {
|
|
||||||
attrNames[name] = struct{}{}
|
|
||||||
}
|
|
||||||
for name := range newType.AttributeTypes() {
|
|
||||||
attrNames[name] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
matches := 0
|
|
||||||
|
|
||||||
for name := range attrNames {
|
|
||||||
if !oldType.HasAttribute(name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !newType.HasAttribute(name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
eq := old.GetAttr(name).Equals(new.GetAttr(name))
|
|
||||||
if !eq.IsKnown() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if eq.True() {
|
|
||||||
matches++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return float64(matches) / float64(len(attrNames))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ctyEqualWithUnknown(old, new cty.Value) bool {
|
func ctyEqualWithUnknown(old, new cty.Value) bool {
|
||||||
if !old.IsWhollyKnown() || !new.IsWhollyKnown() {
|
if !old.IsWhollyKnown() || !new.IsWhollyKnown() {
|
||||||
return false
|
return false
|
||||||
|
@ -1073,6 +1061,19 @@ func ctyEqualWithUnknown(old, new cty.Value) bool {
|
||||||
return old.Equals(new).True()
|
return old.Equals(new).True()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ctyTypesEqual checks equality of two types more loosely
|
||||||
|
// by avoiding checks of object/tuple elements
|
||||||
|
// as we render differences on element-by-element basis anyway
|
||||||
|
func ctyTypesEqual(oldT, newT cty.Type) bool {
|
||||||
|
if oldT.IsObjectType() && newT.IsObjectType() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if oldT.IsTupleType() && newT.IsTupleType() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return oldT.Equals(newT)
|
||||||
|
}
|
||||||
|
|
||||||
func ctyEnsurePathCapacity(path cty.Path, minExtra int) cty.Path {
|
func ctyEnsurePathCapacity(path cty.Path, minExtra int) cty.Path {
|
||||||
if cap(path)-len(path) >= minExtra {
|
if cap(path)-len(path) >= minExtra {
|
||||||
return path
|
return path
|
||||||
|
|
|
@ -286,7 +286,7 @@ func TestResourceChange_JSON(t *testing.T) {
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
"in-place update": {
|
"in-place update of object": {
|
||||||
Action: plans.Update,
|
Action: plans.Update,
|
||||||
Mode: addrs.ManagedResourceMode,
|
Mode: addrs.ManagedResourceMode,
|
||||||
Before: cty.ObjectVal(map[string]cty.Value{
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
@ -309,13 +309,108 @@ func TestResourceChange_JSON(t *testing.T) {
|
||||||
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
||||||
~ json_field = jsonencode(
|
~ json_field = jsonencode(
|
||||||
~ {
|
~ {
|
||||||
- aaa = "value"
|
aaa = "value"
|
||||||
} -> {
|
|
||||||
+ aaa = "value"
|
|
||||||
+ bbb = "new_value"
|
+ bbb = "new_value"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"in-place update (from empty tuple)": {
|
||||||
|
Action: plans.Update,
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||||
|
"json_field": cty.StringVal(`{"aaa": []}`),
|
||||||
|
}),
|
||||||
|
After: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.UnknownVal(cty.String),
|
||||||
|
"json_field": cty.StringVal(`{"aaa": ["value"]}`),
|
||||||
|
}),
|
||||||
|
Schema: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||||
|
"json_field": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RequiredReplace: cty.NewPathSet(),
|
||||||
|
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||||
|
~ resource "test_instance" "example" {
|
||||||
|
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
||||||
|
~ json_field = jsonencode(
|
||||||
|
~ {
|
||||||
|
~ aaa = [
|
||||||
|
+ "value",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"in-place update (to empty tuple)": {
|
||||||
|
Action: plans.Update,
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||||
|
"json_field": cty.StringVal(`{"aaa": ["value"]}`),
|
||||||
|
}),
|
||||||
|
After: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.UnknownVal(cty.String),
|
||||||
|
"json_field": cty.StringVal(`{"aaa": []}`),
|
||||||
|
}),
|
||||||
|
Schema: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||||
|
"json_field": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RequiredReplace: cty.NewPathSet(),
|
||||||
|
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||||
|
~ resource "test_instance" "example" {
|
||||||
|
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
||||||
|
~ json_field = jsonencode(
|
||||||
|
~ {
|
||||||
|
~ aaa = [
|
||||||
|
- "value",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"in-place update (tuple of different types)": {
|
||||||
|
Action: plans.Update,
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||||
|
"json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`),
|
||||||
|
}),
|
||||||
|
After: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.UnknownVal(cty.String),
|
||||||
|
"json_field": cty.StringVal(`{"aaa": [42, {"foo":"baz"}, "value"]}`),
|
||||||
|
}),
|
||||||
|
Schema: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||||
|
"json_field": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RequiredReplace: cty.NewPathSet(),
|
||||||
|
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||||
|
~ resource "test_instance" "example" {
|
||||||
|
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
||||||
|
~ json_field = jsonencode(
|
||||||
|
~ {
|
||||||
|
~ aaa = [
|
||||||
|
42,
|
||||||
|
~ {
|
||||||
|
~ foo = "bar" -> "baz"
|
||||||
|
},
|
||||||
|
"value",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
"force-new update": {
|
"force-new update": {
|
||||||
|
@ -343,9 +438,7 @@ func TestResourceChange_JSON(t *testing.T) {
|
||||||
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
||||||
~ json_field = jsonencode(
|
~ json_field = jsonencode(
|
||||||
~ {
|
~ {
|
||||||
- aaa = "value"
|
aaa = "value"
|
||||||
} -> {
|
|
||||||
+ aaa = "value"
|
|
||||||
+ bbb = "new_value"
|
+ bbb = "new_value"
|
||||||
} # forces replacement
|
} # forces replacement
|
||||||
)
|
)
|
||||||
|
@ -436,6 +529,279 @@ func TestResourceChange_JSON(t *testing.T) {
|
||||||
+ id = (known after apply)
|
+ id = (known after apply)
|
||||||
+ json_field = jsonencode({})
|
+ json_field = jsonencode({})
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"JSON list item removal": {
|
||||||
|
Action: plans.Update,
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||||
|
"json_field": cty.StringVal(`["first","second","third"]`),
|
||||||
|
}),
|
||||||
|
After: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.UnknownVal(cty.String),
|
||||||
|
"json_field": cty.StringVal(`["first","second"]`),
|
||||||
|
}),
|
||||||
|
Schema: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||||
|
"json_field": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RequiredReplace: cty.NewPathSet(),
|
||||||
|
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||||
|
~ resource "test_instance" "example" {
|
||||||
|
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
||||||
|
~ json_field = jsonencode(
|
||||||
|
~ [
|
||||||
|
"first",
|
||||||
|
"second",
|
||||||
|
- "third",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"JSON list item addition": {
|
||||||
|
Action: plans.Update,
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||||
|
"json_field": cty.StringVal(`["first","second"]`),
|
||||||
|
}),
|
||||||
|
After: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.UnknownVal(cty.String),
|
||||||
|
"json_field": cty.StringVal(`["first","second","third"]`),
|
||||||
|
}),
|
||||||
|
Schema: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||||
|
"json_field": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RequiredReplace: cty.NewPathSet(),
|
||||||
|
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||||
|
~ resource "test_instance" "example" {
|
||||||
|
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
||||||
|
~ json_field = jsonencode(
|
||||||
|
~ [
|
||||||
|
"first",
|
||||||
|
"second",
|
||||||
|
+ "third",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"JSON list object addition": {
|
||||||
|
Action: plans.Update,
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||||
|
"json_field": cty.StringVal(`{"first":"111"}`),
|
||||||
|
}),
|
||||||
|
After: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.UnknownVal(cty.String),
|
||||||
|
"json_field": cty.StringVal(`{"first":"111","second":"222"}`),
|
||||||
|
}),
|
||||||
|
Schema: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||||
|
"json_field": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RequiredReplace: cty.NewPathSet(),
|
||||||
|
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||||
|
~ resource "test_instance" "example" {
|
||||||
|
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
||||||
|
~ json_field = jsonencode(
|
||||||
|
~ {
|
||||||
|
first = "111"
|
||||||
|
+ second = "222"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"JSON object with nested list": {
|
||||||
|
Action: plans.Update,
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||||
|
"json_field": cty.StringVal(`{
|
||||||
|
"Statement": ["first"]
|
||||||
|
}`),
|
||||||
|
}),
|
||||||
|
After: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.UnknownVal(cty.String),
|
||||||
|
"json_field": cty.StringVal(`{
|
||||||
|
"Statement": ["first", "second"]
|
||||||
|
}`),
|
||||||
|
}),
|
||||||
|
Schema: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||||
|
"json_field": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RequiredReplace: cty.NewPathSet(),
|
||||||
|
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||||
|
~ resource "test_instance" "example" {
|
||||||
|
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
||||||
|
~ json_field = jsonencode(
|
||||||
|
~ {
|
||||||
|
~ Statement = [
|
||||||
|
"first",
|
||||||
|
+ "second",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"JSON list of objects": {
|
||||||
|
Action: plans.Update,
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||||
|
"json_field": cty.StringVal(`[{"one": "111"}]`),
|
||||||
|
}),
|
||||||
|
After: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.UnknownVal(cty.String),
|
||||||
|
"json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}]`),
|
||||||
|
}),
|
||||||
|
Schema: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||||
|
"json_field": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RequiredReplace: cty.NewPathSet(),
|
||||||
|
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||||
|
~ resource "test_instance" "example" {
|
||||||
|
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
||||||
|
~ json_field = jsonencode(
|
||||||
|
~ [
|
||||||
|
{
|
||||||
|
one = "111"
|
||||||
|
},
|
||||||
|
+ {
|
||||||
|
+ two = "222"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"JSON object with list of objects": {
|
||||||
|
Action: plans.Update,
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||||
|
"json_field": cty.StringVal(`{"parent":[{"one": "111"}]}`),
|
||||||
|
}),
|
||||||
|
After: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.UnknownVal(cty.String),
|
||||||
|
"json_field": cty.StringVal(`{"parent":[{"one": "111"}, {"two": "222"}]}`),
|
||||||
|
}),
|
||||||
|
Schema: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||||
|
"json_field": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RequiredReplace: cty.NewPathSet(),
|
||||||
|
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||||
|
~ resource "test_instance" "example" {
|
||||||
|
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
||||||
|
~ json_field = jsonencode(
|
||||||
|
~ {
|
||||||
|
~ parent = [
|
||||||
|
{
|
||||||
|
one = "111"
|
||||||
|
},
|
||||||
|
+ {
|
||||||
|
+ two = "222"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"JSON object double nested lists": {
|
||||||
|
Action: plans.Update,
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||||
|
"json_field": cty.StringVal(`{"parent":[{"another_list": ["111"]}]}`),
|
||||||
|
}),
|
||||||
|
After: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.UnknownVal(cty.String),
|
||||||
|
"json_field": cty.StringVal(`{"parent":[{"another_list": ["111", "222"]}]}`),
|
||||||
|
}),
|
||||||
|
Schema: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||||
|
"json_field": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RequiredReplace: cty.NewPathSet(),
|
||||||
|
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||||
|
~ resource "test_instance" "example" {
|
||||||
|
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
||||||
|
~ json_field = jsonencode(
|
||||||
|
~ {
|
||||||
|
~ parent = [
|
||||||
|
~ {
|
||||||
|
~ another_list = [
|
||||||
|
"111",
|
||||||
|
+ "222",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"in-place update from object to tuple": {
|
||||||
|
Action: plans.Update,
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||||
|
"json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`),
|
||||||
|
}),
|
||||||
|
After: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.UnknownVal(cty.String),
|
||||||
|
"json_field": cty.StringVal(`["aaa", 42, "something"]`),
|
||||||
|
}),
|
||||||
|
Schema: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||||
|
"json_field": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RequiredReplace: cty.NewPathSet(),
|
||||||
|
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||||
|
~ resource "test_instance" "example" {
|
||||||
|
~ id = "i-02ae66f368e8518a9" -> (known after apply)
|
||||||
|
~ json_field = jsonencode(
|
||||||
|
~ {
|
||||||
|
- aaa = [
|
||||||
|
- 42,
|
||||||
|
- {
|
||||||
|
- foo = "bar"
|
||||||
|
},
|
||||||
|
- "value",
|
||||||
|
]
|
||||||
|
} -> [
|
||||||
|
+ "aaa",
|
||||||
|
+ 42,
|
||||||
|
+ "something",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue