Merge pull request #20071 from hashicorp/b-fix-json-diff-formatting

command/format: Fix nested (JSON) object formatting
This commit is contained in:
Radek Simko 2019-01-23 15:37:03 +00:00 committed by GitHub
commit 71d07832e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 503 additions and 136 deletions

View File

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

View File

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