diff --git a/internal/command/jsonformat/change/renderer_test.go b/internal/command/jsonformat/change/renderer_test.go index 15f89e4044..2596a9fb7e 100644 --- a/internal/command/jsonformat/change/renderer_test.go +++ b/internal/command/jsonformat/change/renderer_test.go @@ -1471,6 +1471,44 @@ func TestRenderers(t *testing.T) { # (2 unchanged blocks hidden) }`, }, + "output_map_to_list": { + change: Change{ + renderer: TypeChange(Change{ + renderer: Map(map[string]Change{ + "element_one": { + renderer: Primitive(strptr("0"), nil), + action: plans.Delete, + }, + "element_two": { + renderer: Primitive(strptr("1"), nil), + action: plans.Delete, + }, + }), + action: plans.Delete, + }, Change{ + renderer: List([]Change{ + { + renderer: Primitive(nil, strptr("0")), + action: plans.Create, + }, + { + renderer: Primitive(nil, strptr("1")), + action: plans.Create, + }, + }), + action: plans.Create, + }), + }, + expected: ` +{ + - "element_one" = 0 + - "element_two" = 1 + } -> [ + + 0, + + 1, + ] +`, + }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { diff --git a/internal/command/jsonformat/change/testing.go b/internal/command/jsonformat/change/testing.go index 11368eefc7..d442275a92 100644 --- a/internal/command/jsonformat/change/testing.go +++ b/internal/command/jsonformat/change/testing.go @@ -243,6 +243,20 @@ func ValidateBlock(attributes map[string]ValidateChangeFunc, blocks map[string][ } } +func ValidateTypeChange(before, after ValidateChangeFunc, action plans.Action, replace bool) ValidateChangeFunc { + return func(t *testing.T, change Change) { + validateChange(t, change, action, replace) + + typeChange, ok := change.renderer.(*typeChangeRenderer) + if !ok { + t.Fatalf("invalid renderer type: %T", change.renderer) + } + + before(t, typeChange.before) + after(t, typeChange.after) + } +} + func ValidateSensitive(before, after interface{}, beforeSensitive, afterSensitive bool, action plans.Action, replace bool) ValidateChangeFunc { return func(t *testing.T, change Change) { validateChange(t, change, action, replace) diff --git a/internal/command/jsonformat/change/type_change.go b/internal/command/jsonformat/change/type_change.go new file mode 100644 index 0000000000..8a886a2bca --- /dev/null +++ b/internal/command/jsonformat/change/type_change.go @@ -0,0 +1,22 @@ +package change + +import "fmt" + +func TypeChange(before, after Change) Renderer { + return &typeChangeRenderer{ + before: before, + after: after, + } +} + +type typeChangeRenderer struct { + NoWarningsRenderer + + before Change + after Change +} + +func (renderer typeChangeRenderer) Render(change Change, indent int, opts RenderOpts) string { + opts.overrideNullSuffix = true // Never render null suffix for children of type changes. + return fmt.Sprintf("%s [yellow]->[reset] %s", renderer.before.Render(indent, opts), renderer.after.Render(indent, opts)) +} diff --git a/internal/command/jsonformat/differ/attribute.go b/internal/command/jsonformat/differ/attribute.go index 7efd9fa9e1..b91588205c 100644 --- a/internal/command/jsonformat/differ/attribute.go +++ b/internal/command/jsonformat/differ/attribute.go @@ -31,6 +31,10 @@ func (v Value) computeChangeForNestedAttribute(attribute *jsonprovider.NestedTyp } func (v Value) computeChangeForType(ctyType cty.Type) change.Change { + if ctyType == cty.NilType { + return v.ComputeChangeForOutput() + } + switch { case ctyType.IsPrimitiveType(): return v.computeAttributeChangeAsPrimitive(ctyType) diff --git a/internal/command/jsonformat/differ/output.go b/internal/command/jsonformat/differ/output.go index fe85bb1053..392f822895 100644 --- a/internal/command/jsonformat/differ/output.go +++ b/internal/command/jsonformat/differ/output.go @@ -1,7 +1,86 @@ package differ -import "github.com/hashicorp/terraform/internal/command/jsonformat/change" +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/command/jsonformat/change" + "github.com/hashicorp/terraform/internal/plans" + "github.com/zclconf/go-cty/cty" +) + +const ( + jsonNumber = "number" + jsonObject = "object" + jsonArray = "array" + jsonBool = "bool" + jsonString = "string" + jsonNull = "null" +) func (v Value) ComputeChangeForOutput() change.Change { - panic("not implemented") + beforeType := getJsonType(v.Before) + afterType := getJsonType(v.After) + + valueToAttribute := func(v Value, jsonType string) change.Change { + var res change.Change + + switch jsonType { + case jsonNull: + res = v.computeAttributeChangeAsPrimitive(cty.NilType) + case jsonBool: + res = v.computeAttributeChangeAsPrimitive(cty.Bool) + case jsonString: + res = v.computeAttributeChangeAsPrimitive(cty.String) + case jsonNumber: + res = v.computeAttributeChangeAsPrimitive(cty.Number) + case jsonObject: + res = v.computeAttributeChangeAsMap(cty.NilType) + case jsonArray: + res = v.computeAttributeChangeAsList(cty.NilType) + default: + panic("unrecognized json type: " + jsonType) + } + + return res + } + + if beforeType == afterType || (beforeType == jsonNull || afterType == jsonNull) { + targetType := beforeType + if targetType == jsonNull { + targetType = afterType + } + return valueToAttribute(v, targetType) + } + + before := valueToAttribute(Value{ + Before: v.Before, + BeforeSensitive: v.BeforeSensitive, + }, beforeType) + + after := valueToAttribute(Value{ + After: v.After, + AfterSensitive: v.AfterSensitive, + Unknown: v.Unknown, + }, afterType) + + return change.New(change.TypeChange(before, after), plans.Update, false) +} + +func getJsonType(json interface{}) string { + switch json.(type) { + case []interface{}: + return jsonArray + case float64: + return jsonNumber + case string: + return jsonString + case bool: + return jsonBool + case nil: + return jsonNull + case map[string]interface{}: + return jsonObject + default: + panic(fmt.Sprintf("unrecognized json type %T", json)) + } } diff --git a/internal/command/jsonformat/differ/value_test.go b/internal/command/jsonformat/differ/value_test.go index 7f39d4c59d..c1759f9e5a 100644 --- a/internal/command/jsonformat/differ/value_test.go +++ b/internal/command/jsonformat/differ/value_test.go @@ -1208,6 +1208,231 @@ func TestValue_BlockAttributesAndNestedBlocks(t *testing.T) { } } +func TestValue_Outputs(t *testing.T) { + tcs := map[string]struct { + input Value + validateChange change.ValidateChangeFunc + }{ + "primitive_create": { + input: Value{ + Before: nil, + After: "new", + }, + validateChange: change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), + }, + "map_create": { + input: Value{ + Before: nil, + After: map[string]interface{}{ + "element_one": "new_one", + "element_two": "new_two", + }, + }, + validateChange: change.ValidateMap(map[string]change.ValidateChangeFunc{ + "element_one": change.ValidatePrimitive(nil, strptr("\"new_one\""), plans.Create, false), + "element_two": change.ValidatePrimitive(nil, strptr("\"new_two\""), plans.Create, false), + }, plans.Create, false), + }, + "list_create": { + input: Value{ + Before: nil, + After: []interface{}{ + "new_one", + "new_two", + }, + }, + validateChange: change.ValidateList([]change.ValidateChangeFunc{ + change.ValidatePrimitive(nil, strptr("\"new_one\""), plans.Create, false), + change.ValidatePrimitive(nil, strptr("\"new_two\""), plans.Create, false), + }, plans.Create, false), + }, + "primitive_update": { + input: Value{ + Before: "old", + After: "new", + }, + validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, false), + }, + "map_update": { + input: Value{ + Before: map[string]interface{}{ + "element_one": "old_one", + "element_two": "old_two", + }, + After: map[string]interface{}{ + "element_one": "new_one", + "element_two": "new_two", + }, + }, + validateChange: change.ValidateMap(map[string]change.ValidateChangeFunc{ + "element_one": change.ValidatePrimitive(strptr("\"old_one\""), strptr("\"new_one\""), plans.Update, false), + "element_two": change.ValidatePrimitive(strptr("\"old_two\""), strptr("\"new_two\""), plans.Update, false), + }, plans.Update, false), + }, + "list_update": { + input: Value{ + Before: []interface{}{ + "old_one", + "old_two", + }, + After: []interface{}{ + "new_one", + "new_two", + }, + }, + validateChange: change.ValidateList([]change.ValidateChangeFunc{ + change.ValidatePrimitive(strptr("\"old_one\""), nil, plans.Delete, false), + change.ValidatePrimitive(strptr("\"old_two\""), nil, plans.Delete, false), + change.ValidatePrimitive(nil, strptr("\"new_one\""), plans.Create, false), + change.ValidatePrimitive(nil, strptr("\"new_two\""), plans.Create, false), + }, plans.Update, false), + }, + "primitive_delete": { + input: Value{ + Before: "old", + After: nil, + }, + validateChange: change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + }, + "map_delete": { + input: Value{ + Before: map[string]interface{}{ + "element_one": "old_one", + "element_two": "old_two", + }, + After: nil, + }, + validateChange: change.ValidateMap(map[string]change.ValidateChangeFunc{ + "element_one": change.ValidatePrimitive(strptr("\"old_one\""), nil, plans.Delete, false), + "element_two": change.ValidatePrimitive(strptr("\"old_two\""), nil, plans.Delete, false), + }, plans.Delete, false), + }, + "list_delete": { + input: Value{ + Before: []interface{}{ + "old_one", + "old_two", + }, + After: nil, + }, + validateChange: change.ValidateList([]change.ValidateChangeFunc{ + change.ValidatePrimitive(strptr("\"old_one\""), nil, plans.Delete, false), + change.ValidatePrimitive(strptr("\"old_two\""), nil, plans.Delete, false), + }, plans.Delete, false), + }, + "primitive_to_list": { + input: Value{ + Before: "old", + After: []interface{}{ + "new_one", + "new_two", + }, + }, + validateChange: change.ValidateTypeChange( + change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + change.ValidateList([]change.ValidateChangeFunc{ + change.ValidatePrimitive(nil, strptr("\"new_one\""), plans.Create, false), + change.ValidatePrimitive(nil, strptr("\"new_two\""), plans.Create, false), + }, plans.Create, false), plans.Update, false), + }, + "primitive_to_map": { + input: Value{ + Before: "old", + After: map[string]interface{}{ + "element_one": "new_one", + "element_two": "new_two", + }, + }, + validateChange: change.ValidateTypeChange( + change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + change.ValidateMap(map[string]change.ValidateChangeFunc{ + "element_one": change.ValidatePrimitive(nil, strptr("\"new_one\""), plans.Create, false), + "element_two": change.ValidatePrimitive(nil, strptr("\"new_two\""), plans.Create, false), + }, plans.Create, false), plans.Update, false), + }, + "list_to_primitive": { + input: Value{ + Before: []interface{}{ + "old_one", + "old_two", + }, + After: "new", + }, + validateChange: change.ValidateTypeChange( + change.ValidateList([]change.ValidateChangeFunc{ + change.ValidatePrimitive(strptr("\"old_one\""), nil, plans.Delete, false), + change.ValidatePrimitive(strptr("\"old_two\""), nil, plans.Delete, false), + }, plans.Delete, false), + change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), + plans.Update, false), + }, + "list_to_map": { + input: Value{ + Before: []interface{}{ + "old_one", + "old_two", + }, + After: map[string]interface{}{ + "element_one": "new_one", + "element_two": "new_two", + }, + }, + validateChange: change.ValidateTypeChange( + change.ValidateList([]change.ValidateChangeFunc{ + change.ValidatePrimitive(strptr("\"old_one\""), nil, plans.Delete, false), + change.ValidatePrimitive(strptr("\"old_two\""), nil, plans.Delete, false), + }, plans.Delete, false), + change.ValidateMap(map[string]change.ValidateChangeFunc{ + "element_one": change.ValidatePrimitive(nil, strptr("\"new_one\""), plans.Create, false), + "element_two": change.ValidatePrimitive(nil, strptr("\"new_two\""), plans.Create, false), + }, plans.Create, false), plans.Update, false), + }, + "map_to_primitive": { + input: Value{ + Before: map[string]interface{}{ + "element_one": "old_one", + "element_two": "old_two", + }, + After: "new", + }, + validateChange: change.ValidateTypeChange( + change.ValidateMap(map[string]change.ValidateChangeFunc{ + "element_one": change.ValidatePrimitive(strptr("\"old_one\""), nil, plans.Delete, false), + "element_two": change.ValidatePrimitive(strptr("\"old_two\""), nil, plans.Delete, false), + }, plans.Delete, false), + change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), + plans.Update, false), + }, + "map_to_list": { + input: Value{ + Before: map[string]interface{}{ + "element_one": "old_one", + "element_two": "old_two", + }, + After: []interface{}{ + "new_one", + "new_two", + }, + }, + validateChange: change.ValidateTypeChange( + change.ValidateMap(map[string]change.ValidateChangeFunc{ + "element_one": change.ValidatePrimitive(strptr("\"old_one\""), nil, plans.Delete, false), + "element_two": change.ValidatePrimitive(strptr("\"old_two\""), nil, plans.Delete, false), + }, plans.Delete, false), + change.ValidateList([]change.ValidateChangeFunc{ + change.ValidatePrimitive(nil, strptr("\"new_one\""), plans.Create, false), + change.ValidatePrimitive(nil, strptr("\"new_two\""), plans.Create, false), + }, plans.Create, false), plans.Update, false), + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + tc.validateChange(t, tc.input.ComputeChange(cty.NilType)) + }) + } +} + func TestValue_PrimitiveAttributes(t *testing.T) { // This function tests manipulating primitives: creating, deleting and // updating. It also automatically tests these operations within the