diff --git a/command/format/diff.go b/command/format/diff.go index da5fabd264..049f625434 100644 --- a/command/format/diff.go +++ b/command/format/diff.go @@ -556,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) { ty := old.Type() + typesEqual := ctyTypesEqual(ty, new.Type()) // We have some specialized diff implementations for certain complex // values where it's useful to see a visualization of the diff of @@ -563,7 +564,7 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa // new values verbatim. // However, these specialized implementations can apply only if both // 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 { case ty == cty.String: // We have special behavior for both multi-line strings in general @@ -1060,6 +1061,19 @@ func ctyEqualWithUnknown(old, new cty.Value) bool { 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 { if cap(path)-len(path) >= minExtra { return path diff --git a/command/format/diff_test.go b/command/format/diff_test.go index 0c34fc3233..5088e76317 100644 --- a/command/format/diff_test.go +++ b/command/format/diff_test.go @@ -286,7 +286,7 @@ func TestResourceChange_JSON(t *testing.T) { } `, }, - "in-place update": { + "in-place update of object": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ @@ -763,6 +763,45 @@ func TestResourceChange_JSON(t *testing.T) { } ) } +`, + }, + "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", + ] + ) + } `, }, }