diff --git a/internal/command/jsonformat/change/list.go b/internal/command/jsonformat/change/list.go index b169c4c6df..7ac66be735 100644 --- a/internal/command/jsonformat/change/list.go +++ b/internal/command/jsonformat/change/list.go @@ -24,9 +24,6 @@ func NestedList(elements []Change) Renderer { type listRenderer struct { NoWarningsRenderer - // displayContext tells the renderer to display additional information about - // the before and after index values within a given list. For example, index - // displayContext bool elements []Change } diff --git a/internal/command/jsonformat/change/renderer_test.go b/internal/command/jsonformat/change/renderer_test.go index 29368fda75..2548e5fb35 100644 --- a/internal/command/jsonformat/change/renderer_test.go +++ b/internal/command/jsonformat/change/renderer_test.go @@ -889,6 +889,272 @@ func TestRenderers(t *testing.T) { [ ~ 0 -> (known after apply), ] +`, + }, + "set_create_empty": { + change: Change{ + renderer: Set([]Change{}), + action: plans.Create, + }, + expected: "[]", + }, + "set_create": { + change: Change{ + renderer: Set([]Change{ + { + renderer: Primitive(nil, strptr("1")), + action: plans.Create, + }, + }), + action: plans.Create, + }, + expected: ` +[ + + 1, + ] +`, + }, + "set_delete_empty": { + change: Change{ + renderer: Set([]Change{}), + action: plans.Delete, + }, + expected: "[] -> null", + }, + "set_delete": { + change: Change{ + renderer: Set([]Change{ + { + renderer: Primitive(strptr("1"), nil), + action: plans.Delete, + }, + }), + action: plans.Delete, + }, + expected: ` +[ + - 1, + ] -> null +`, + }, + "set_create_element": { + change: Change{ + renderer: Set([]Change{ + { + renderer: Primitive(nil, strptr("1")), + action: plans.Create, + }, + }), + action: plans.Update, + }, + expected: ` +[ + + 1, + ] +`, + }, + "set_update_element": { + change: Change{ + renderer: Set([]Change{ + { + renderer: Primitive(strptr("0"), strptr("1")), + action: plans.Update, + }, + }), + action: plans.Update, + }, + expected: ` +[ + ~ 0 -> 1, + ] +`, + }, + "set_replace_element": { + change: Change{ + renderer: Set([]Change{ + { + renderer: Primitive(strptr("0"), nil), + action: plans.Delete, + }, + { + renderer: Primitive(nil, strptr("1")), + action: plans.Create, + }, + }), + action: plans.Update, + }, + expected: ` +[ + - 0, + + 1, + ] +`, + }, + "set_delete_element": { + change: Change{ + renderer: Set([]Change{ + { + renderer: Primitive(strptr("0"), nil), + action: plans.Delete, + }, + }), + action: plans.Update, + }, + expected: ` +[ + - 0, + ] +`, + }, + "set_update_forces_replacement": { + change: Change{ + renderer: Set([]Change{ + { + renderer: Primitive(strptr("0"), strptr("1")), + action: plans.Update, + }, + }), + action: plans.Update, + replace: true, + }, + expected: ` +[ # forces replacement + ~ 0 -> 1, + ] +`, + }, + "set_update_ignores_unchanged": { + change: Change{ + renderer: Set([]Change{ + { + renderer: Primitive(strptr("0"), strptr("0")), + action: plans.NoOp, + }, + { + renderer: Primitive(strptr("1"), strptr("1")), + action: plans.NoOp, + }, + { + renderer: Primitive(strptr("2"), strptr("5")), + action: plans.Update, + }, + { + renderer: Primitive(strptr("3"), strptr("3")), + action: plans.NoOp, + }, + { + renderer: Primitive(strptr("4"), strptr("4")), + action: plans.NoOp, + }, + }), + action: plans.Update, + }, + expected: ` +[ + ~ 2 -> 5, + # (4 unchanged elements hidden) + ] +`, + }, + "set_create_sensitive_element": { + change: Change{ + renderer: Set([]Change{ + { + renderer: Sensitive(nil, 1, false, true), + action: plans.Create, + }, + }), + action: plans.Update, + }, + expected: ` +[ + + (sensitive), + ] +`, + }, + "set_delete_sensitive_element": { + change: Change{ + renderer: Set([]Change{ + { + renderer: Sensitive(1, nil, true, false), + action: plans.Delete, + }, + }), + action: plans.Update, + }, + expected: ` +[ + - (sensitive), + ] +`, + }, + "set_update_sensitive_element": { + change: Change{ + renderer: Set([]Change{ + { + renderer: Sensitive(nil, 1, false, true), + action: plans.Update, + }, + }), + action: plans.Update, + }, + expected: ` +[ + ~ (sensitive), + ] +`, + }, + "set_update_sensitive_element_status": { + change: Change{ + renderer: Set([]Change{ + { + renderer: Sensitive(1, 2, false, true), + action: plans.Update, + }, + }), + action: plans.Update, + }, + expected: ` +[ + # Warning: this attribute value will be marked as sensitive and will not + # display in UI output after applying this change. + ~ (sensitive), + ] +`, + }, + "set_create_computed_element": { + change: Change{ + renderer: Set([]Change{ + { + renderer: Computed(Change{}), + action: plans.Create, + }, + }), + action: plans.Update, + }, + expected: ` +[ + + (known after apply), + ] +`, + }, + "set_update_computed_element": { + change: Change{ + renderer: Set([]Change{ + { + renderer: Computed(Change{ + renderer: Primitive(strptr("0"), nil), + action: plans.Delete, + }), + action: plans.Update, + }, + }), + action: plans.Update, + }, + expected: ` +[ + ~ 0 -> (known after apply), + ] `, }, } diff --git a/internal/command/jsonformat/change/set.go b/internal/command/jsonformat/change/set.go new file mode 100644 index 0000000000..997d39ce06 --- /dev/null +++ b/internal/command/jsonformat/change/set.go @@ -0,0 +1,53 @@ +package change + +import ( + "bytes" + "fmt" + + "github.com/hashicorp/terraform/internal/command/format" + "github.com/hashicorp/terraform/internal/plans" +) + +func Set(elements []Change) Renderer { + return &setRenderer{ + elements: elements, + } +} + +type setRenderer struct { + NoWarningsRenderer + + elements []Change +} + +func (renderer setRenderer) Render(change Change, indent int, opts RenderOpts) string { + if len(renderer.elements) == 0 { + return fmt.Sprintf("[]%s%s", change.nullSuffix(opts.overrideNullSuffix), change.forcesReplacement()) + } + + elementOpts := opts.Clone() + elementOpts.overrideNullSuffix = true + + unchangedElements := 0 + + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("[%s\n", change.forcesReplacement())) + for _, element := range renderer.elements { + if element.action == plans.NoOp && !opts.showUnchangedChildren { + unchangedElements++ + continue + } + + for _, warning := range element.Warnings(indent + 1) { + buf.WriteString(fmt.Sprintf("%s%s\n", change.indent(indent+1), warning)) + } + buf.WriteString(fmt.Sprintf("%s%s %s,\n", change.indent(indent+1), format.DiffActionSymbol(element.action), element.Render(indent+1, elementOpts))) + } + + if unchangedElements > 0 { + buf.WriteString(fmt.Sprintf("%s%s %s\n", change.indent(indent+1), change.emptySymbol(), change.unchanged("element", unchangedElements))) + } + + buf.WriteString(fmt.Sprintf("%s%s ]%s", change.indent(indent), change.emptySymbol(), change.nullSuffix(opts.overrideNullSuffix))) + return buf.String() +} diff --git a/internal/command/jsonformat/change/testing.go b/internal/command/jsonformat/change/testing.go index f7e752d915..51e2536b3c 100644 --- a/internal/command/jsonformat/change/testing.go +++ b/internal/command/jsonformat/change/testing.go @@ -170,6 +170,26 @@ func validateList(t *testing.T, list *listRenderer, elements []ValidateChangeFun } } +func ValidateSet(elements []ValidateChangeFunc, action plans.Action, replace bool) ValidateChangeFunc { + return func(t *testing.T, change Change) { + validateChange(t, change, action, replace) + + set, ok := change.renderer.(*setRenderer) + if !ok { + t.Fatalf("invalid renderer type: %T", change.renderer) + } + + if len(set.elements) != len(elements) { + t.Fatalf("expected %d elements but found %d elements", len(elements), len(set.elements)) + } + + for ix := 0; ix < len(elements); ix++ { + elements[ix](t, set.elements[ix]) + } + + } +} + 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/differ/attribute.go b/internal/command/jsonformat/differ/attribute.go index cd6e506b79..7efd9fa9e1 100644 --- a/internal/command/jsonformat/differ/attribute.go +++ b/internal/command/jsonformat/differ/attribute.go @@ -23,6 +23,8 @@ func (v Value) computeChangeForNestedAttribute(attribute *jsonprovider.NestedTyp return v.computeAttributeChangeAsNestedMap(attribute.Attributes) case "list": return v.computeAttributeChangeAsNestedList(attribute.Attributes) + case "set": + return v.computeAttributeChangeAsNestedSet(attribute.Attributes) default: panic("unrecognized nesting mode: " + attribute.NestingMode) } @@ -38,6 +40,8 @@ func (v Value) computeChangeForType(ctyType cty.Type) change.Change { return v.computeAttributeChangeAsMap(ctyType.ElementType()) case ctyType.IsListType(): return v.computeAttributeChangeAsList(ctyType.ElementType()) + case ctyType.IsSetType(): + return v.computeAttributeChangeAsSet(ctyType.ElementType()) default: panic("unrecognized type: " + ctyType.FriendlyName()) } diff --git a/internal/command/jsonformat/differ/set.go b/internal/command/jsonformat/differ/set.go new file mode 100644 index 0000000000..300419cd97 --- /dev/null +++ b/internal/command/jsonformat/differ/set.go @@ -0,0 +1,90 @@ +package differ + +import ( + "reflect" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/command/jsonformat/change" + "github.com/hashicorp/terraform/internal/command/jsonprovider" +) + +func (v Value) computeAttributeChangeAsSet(elementType cty.Type) change.Change { + var elements []change.Change + current := v.getDefaultActionForIteration() + v.processSet(false, func(value Value) { + element := value.ComputeChange(elementType) + elements = append(elements, element) + current = compareActions(current, element.GetAction()) + }) + return change.New(change.Set(elements), current, v.replacePath()) +} + +func (v Value) computeAttributeChangeAsNestedSet(attributes map[string]*jsonprovider.Attribute) change.Change { + var elements []change.Change + current := v.getDefaultActionForIteration() + v.processSet(true, func(value Value) { + element := value.ComputeChange(attributes) + elements = append(elements, element) + current = compareActions(current, element.GetAction()) + }) + return change.New(change.Set(elements), current, v.replacePath()) +} + +func (v Value) processSet(propagateReplace bool, process func(value Value)) { + sliceValue := v.asSlice() + + foundInBefore := make(map[int]int) + foundInAfter := make(map[int]int) + + // O(n^2) operation here to find matching pairs in the set, so we can make + // the display look pretty. There might be a better way to do this, so look + // here for potential optimisations. + + for ix := 0; ix < len(sliceValue.Before); ix++ { + matched := false + for jx := 0; jx < len(sliceValue.After); jx++ { + if _, ok := foundInAfter[jx]; ok { + // We've already found a match for this after value. + continue + } + + child := sliceValue.getChild(ix, jx, propagateReplace) + if reflect.DeepEqual(child.Before, child.After) && child.isBeforeSensitive() == child.isAfterSensitive() && child.Unknown == nil { + matched = true + foundInBefore[ix] = jx + foundInAfter[jx] = ix + } + } + + if !matched { + foundInBefore[ix] = -1 + } + } + + // Now everything in before should be a key in foundInBefore and a value + // in foundInAfter. If a key is mapped to -1 in foundInBefore it means it + // does not have an equivalent in foundInAfter and so has been deleted. + // Everything in foundInAfter has a matching value in foundInBefore, but + // some values in after may not be in foundInAfter. This means these values + // are newly created. + + for ix := 0; ix < len(sliceValue.Before); ix++ { + if jx := foundInBefore[ix]; jx >= 0 { + child := sliceValue.getChild(ix, jx, propagateReplace) + process(child) + continue + } + child := sliceValue.getChild(ix, len(sliceValue.After), propagateReplace) + process(child) + } + + for jx := 0; jx < len(sliceValue.After); jx++ { + if _, ok := foundInAfter[jx]; ok { + // Then this value was handled in the previous for loop. + continue + } + child := sliceValue.getChild(len(sliceValue.Before), jx, propagateReplace) + process(child) + } +} diff --git a/internal/command/jsonformat/differ/value_test.go b/internal/command/jsonformat/differ/value_test.go index cbd0ca5541..39b8136910 100644 --- a/internal/command/jsonformat/differ/value_test.go +++ b/internal/command/jsonformat/differ/value_test.go @@ -13,6 +13,27 @@ import ( "github.com/hashicorp/terraform/internal/plans" ) +type SetChange struct { + Before SetChangeEntry + After SetChangeEntry +} + +type SetChangeEntry struct { + SingleChange change.ValidateChangeFunc + ObjectChange map[string]change.ValidateChangeFunc + + Replace bool + Action plans.Action +} + +func (entry SetChangeEntry) Validate(obj func(attributes map[string]change.ValidateChangeFunc, action plans.Action, replace bool) change.ValidateChangeFunc) change.ValidateChangeFunc { + if entry.SingleChange != nil { + return entry.SingleChange + } + + return obj(entry.ObjectChange, entry.Action, entry.Replace) +} + func TestValue_ObjectAttributes(t *testing.T) { // This function holds a range of test cases creating, deleting and editing // objects. It is built in such a way that it can automatically test these @@ -28,6 +49,9 @@ func TestValue_ObjectAttributes(t *testing.T) { validateChanges map[string]change.ValidateChangeFunc validateReplace bool validateAction plans.Action + // Sets break changes out differently to the other collections, so they + // have their own entry. + validateSetChanges *SetChange }{ "create": { input: Value{ @@ -119,6 +143,18 @@ func TestValue_ObjectAttributes(t *testing.T) { validateNestedObject: change.ValidateComputed(change.ValidateNestedObject(map[string]change.ValidateChangeFunc{ "attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), }, plans.Delete, false), plans.Update, false), + validateSetChanges: &SetChange{ + Before: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetChangeEntry{ + SingleChange: change.ValidateComputed(nil, plans.Create, false), + }, + }, }, "create_attribute": { input: Value{ @@ -135,6 +171,20 @@ func TestValue_ObjectAttributes(t *testing.T) { }, validateAction: plans.Update, validateReplace: false, + validateSetChanges: &SetChange{ + Before: SetChangeEntry{ + ObjectChange: nil, + Action: plans.Delete, + Replace: false, + }, + After: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, }, "create_attribute_from_explicit_null": { input: Value{ @@ -153,6 +203,20 @@ func TestValue_ObjectAttributes(t *testing.T) { }, validateAction: plans.Update, validateReplace: false, + validateSetChanges: &SetChange{ + Before: SetChangeEntry{ + ObjectChange: nil, + Action: plans.Delete, + Replace: false, + }, + After: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, }, "delete_attribute": { input: Value{ @@ -169,6 +233,20 @@ func TestValue_ObjectAttributes(t *testing.T) { }, validateAction: plans.Update, validateReplace: false, + validateSetChanges: &SetChange{ + Before: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetChangeEntry{ + ObjectChange: nil, + Action: plans.Create, + Replace: false, + }, + }, }, "delete_attribute_to_explicit_null": { input: Value{ @@ -187,6 +265,20 @@ func TestValue_ObjectAttributes(t *testing.T) { }, validateAction: plans.Update, validateReplace: false, + validateSetChanges: &SetChange{ + Before: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetChangeEntry{ + ObjectChange: nil, + Action: plans.Create, + Replace: false, + }, + }, }, "update_attribute": { input: Value{ @@ -205,6 +297,22 @@ func TestValue_ObjectAttributes(t *testing.T) { }, validateAction: plans.Update, validateReplace: false, + validateSetChanges: &SetChange{ + Before: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, }, "create_sensitive_attribute": { input: Value{ @@ -224,6 +332,20 @@ func TestValue_ObjectAttributes(t *testing.T) { }, validateAction: plans.Update, validateReplace: false, + validateSetChanges: &SetChange{ + Before: SetChangeEntry{ + ObjectChange: nil, + Action: plans.Delete, + Replace: false, + }, + After: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidateSensitive(nil, "new", false, true, plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, }, "delete_sensitive_attribute": { input: Value{ @@ -243,6 +365,20 @@ func TestValue_ObjectAttributes(t *testing.T) { }, validateAction: plans.Update, validateReplace: false, + validateSetChanges: &SetChange{ + Before: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidateSensitive("old", nil, true, false, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetChangeEntry{ + ObjectChange: nil, + Action: plans.Create, + Replace: false, + }, + }, }, "update_sensitive_attribute": { input: Value{ @@ -267,6 +403,22 @@ func TestValue_ObjectAttributes(t *testing.T) { }, validateAction: plans.Update, validateReplace: false, + validateSetChanges: &SetChange{ + Before: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidateSensitive("old", nil, true, false, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidateSensitive(nil, "new", false, true, plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, }, "create_computed_attribute": { input: Value{ @@ -284,6 +436,20 @@ func TestValue_ObjectAttributes(t *testing.T) { }, validateAction: plans.Update, validateReplace: false, + validateSetChanges: &SetChange{ + Before: SetChangeEntry{ + ObjectChange: nil, + Action: plans.Delete, + Replace: false, + }, + After: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidateComputed(nil, plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, }, "update_computed_attribute": { input: Value{ @@ -306,6 +472,22 @@ func TestValue_ObjectAttributes(t *testing.T) { }, validateAction: plans.Update, validateReplace: false, + validateSetChanges: &SetChange{ + Before: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidateComputed(nil, plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, }, "ignores_unset_fields": { input: Value{ @@ -339,6 +521,22 @@ func TestValue_ObjectAttributes(t *testing.T) { }, validateAction: plans.Update, validateReplace: true, + validateSetChanges: &SetChange{ + Before: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), + }, + Action: plans.Delete, + Replace: true, + }, + After: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, }, "update_replace_attribute": { input: Value{ @@ -360,6 +558,22 @@ func TestValue_ObjectAttributes(t *testing.T) { }, validateAction: plans.Update, validateReplace: false, + validateSetChanges: &SetChange{ + Before: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, true), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetChangeEntry{ + ObjectChange: map[string]change.ValidateChangeFunc{ + "attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, }, } @@ -450,6 +664,46 @@ func TestValue_ObjectAttributes(t *testing.T) { }, collectionDefaultAction, false) validate(t, input.ComputeChange(attribute)) }) + + t.Run("set", func(t *testing.T) { + attribute := &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.Object(tc.attributes))), + } + + input := wrapValueInSlice(tc.input) + + if tc.validateSetChanges != nil { + validate := change.ValidateSet(func() []change.ValidateChangeFunc { + var ret []change.ValidateChangeFunc + ret = append(ret, tc.validateSetChanges.Before.Validate(change.ValidateObject)) + ret = append(ret, tc.validateSetChanges.After.Validate(change.ValidateObject)) + return ret + }(), collectionDefaultAction, false) + validate(t, input.ComputeChange(attribute)) + return + } + + if tc.validateObject != nil { + validate := change.ValidateSet([]change.ValidateChangeFunc{ + tc.validateObject, + }, collectionDefaultAction, false) + validate(t, input.ComputeChange(attribute)) + return + } + + if tc.validateSingleChange != nil { + validate := change.ValidateSet([]change.ValidateChangeFunc{ + tc.validateSingleChange, + }, collectionDefaultAction, false) + validate(t, input.ComputeChange(attribute)) + return + } + + validate := change.ValidateSet([]change.ValidateChangeFunc{ + change.ValidateObject(tc.validateChanges, tc.validateAction, tc.validateReplace), + }, collectionDefaultAction, false) + validate(t, input.ComputeChange(attribute)) + }) }) t.Run(fmt.Sprintf("nested_%s", name), func(t *testing.T) { @@ -562,6 +816,57 @@ func TestValue_ObjectAttributes(t *testing.T) { }, collectionDefaultAction, false) validate(t, input.ComputeChange(attribute)) }) + + t.Run("set", func(t *testing.T) { + attribute := &jsonprovider.Attribute{ + AttributeNestedType: &jsonprovider.NestedType{ + Attributes: func() map[string]*jsonprovider.Attribute { + attributes := make(map[string]*jsonprovider.Attribute) + for key, attribute := range tc.attributes { + attributes[key] = &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, attribute), + } + } + return attributes + }(), + NestingMode: "set", + }, + } + + input := wrapValueInSlice(tc.input) + + if tc.validateSetChanges != nil { + validate := change.ValidateSet(func() []change.ValidateChangeFunc { + var ret []change.ValidateChangeFunc + ret = append(ret, tc.validateSetChanges.Before.Validate(change.ValidateNestedObject)) + ret = append(ret, tc.validateSetChanges.After.Validate(change.ValidateNestedObject)) + return ret + }(), collectionDefaultAction, false) + validate(t, input.ComputeChange(attribute)) + return + } + + if tc.validateNestedObject != nil { + validate := change.ValidateSet([]change.ValidateChangeFunc{ + tc.validateNestedObject, + }, collectionDefaultAction, false) + validate(t, input.ComputeChange(attribute)) + return + } + + if tc.validateSingleChange != nil { + validate := change.ValidateSet([]change.ValidateChangeFunc{ + tc.validateSingleChange, + }, collectionDefaultAction, false) + validate(t, input.ComputeChange(attribute)) + return + } + + validate := change.ValidateSet([]change.ValidateChangeFunc{ + change.ValidateNestedObject(tc.validateChanges, tc.validateAction, tc.validateReplace), + }, collectionDefaultAction, false) + validate(t, input.ComputeChange(attribute)) + }) }) } } @@ -572,10 +877,10 @@ func TestValue_PrimitiveAttributes(t *testing.T) { // contexts of collections. tcs := map[string]struct { - input Value - attribute cty.Type - validateChange change.ValidateChangeFunc - validateListChanges []change.ValidateChangeFunc // Lists are special in some cases. + input Value + attribute cty.Type + validateChange change.ValidateChangeFunc + validateSliceChanges []change.ValidateChangeFunc // Lists are special in some cases. }{ "primitive_create": { input: Value{ @@ -598,7 +903,7 @@ func TestValue_PrimitiveAttributes(t *testing.T) { }, attribute: cty.String, validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, false), - validateListChanges: []change.ValidateChangeFunc{ + validateSliceChanges: []change.ValidateChangeFunc{ change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), }, @@ -611,7 +916,7 @@ func TestValue_PrimitiveAttributes(t *testing.T) { }, attribute: cty.String, validateChange: change.ValidatePrimitive(strptr("\"old\""), nil, plans.Update, false), - validateListChanges: []change.ValidateChangeFunc{ + validateSliceChanges: []change.ValidateChangeFunc{ change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), change.ValidatePrimitive(nil, nil, plans.Create, false), }, @@ -624,7 +929,7 @@ func TestValue_PrimitiveAttributes(t *testing.T) { }, attribute: cty.String, validateChange: change.ValidatePrimitive(nil, strptr("\"new\""), plans.Update, false), - validateListChanges: []change.ValidateChangeFunc{ + validateSliceChanges: []change.ValidateChangeFunc{ change.ValidatePrimitive(nil, nil, plans.Delete, false), change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), }, @@ -656,7 +961,7 @@ func TestValue_PrimitiveAttributes(t *testing.T) { }, attribute: cty.String, validateChange: change.ValidateSensitive("old", "new", true, true, plans.Update, false), - validateListChanges: []change.ValidateChangeFunc{ + validateSliceChanges: []change.ValidateChangeFunc{ change.ValidateSensitive("old", nil, true, false, plans.Delete, false), change.ValidateSensitive(nil, "new", false, true, plans.Create, false), }, @@ -678,7 +983,7 @@ func TestValue_PrimitiveAttributes(t *testing.T) { }, attribute: cty.String, validateChange: change.ValidateComputed(change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), plans.Update, false), - validateListChanges: []change.ValidateChangeFunc{ + validateSliceChanges: []change.ValidateChangeFunc{ change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), change.ValidateComputed(nil, plans.Create, false), }, @@ -693,7 +998,7 @@ func TestValue_PrimitiveAttributes(t *testing.T) { }, attribute: cty.String, validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, true), - validateListChanges: []change.ValidateChangeFunc{ + validateSliceChanges: []change.ValidateChangeFunc{ change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, true), change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false), }, @@ -740,8 +1045,8 @@ func TestValue_PrimitiveAttributes(t *testing.T) { AttributeType: unmarshalType(t, cty.List(tc.attribute)), } - if tc.validateListChanges != nil { - validate := change.ValidateList(tc.validateListChanges, defaultCollectionsAction, false) + if tc.validateSliceChanges != nil { + validate := change.ValidateList(tc.validateSliceChanges, defaultCollectionsAction, false) validate(t, input.ComputeChange(attribute)) return } @@ -751,6 +1056,24 @@ func TestValue_PrimitiveAttributes(t *testing.T) { }, defaultCollectionsAction, false) validate(t, input.ComputeChange(attribute)) }) + + t.Run("set", func(t *testing.T) { + input := wrapValueInSlice(tc.input) + attribute := &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(tc.attribute)), + } + + if tc.validateSliceChanges != nil { + validate := change.ValidateSet(tc.validateSliceChanges, defaultCollectionsAction, false) + validate(t, input.ComputeChange(attribute)) + return + } + + validate := change.ValidateSet([]change.ValidateChangeFunc{ + tc.validateChange, + }, defaultCollectionsAction, false) + validate(t, input.ComputeChange(attribute)) + }) }) } } @@ -978,6 +1301,108 @@ func TestValue_CollectionAttributes(t *testing.T) { }, validateChange: change.ValidateComputed(change.ValidateList(nil, plans.Delete, false), plans.Update, false), }, + "set_create_empty": { + input: Value{ + Before: nil, + After: []interface{}{}, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateChange: change.ValidateSet(nil, plans.Create, false), + }, + "set_create_populated": { + input: Value{ + Before: nil, + After: []interface{}{"one", "two"}, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateChange: change.ValidateSet([]change.ValidateChangeFunc{ + change.ValidatePrimitive(nil, strptr("\"one\""), plans.Create, false), + change.ValidatePrimitive(nil, strptr("\"two\""), plans.Create, false), + }, plans.Create, false), + }, + "set_delete_empty": { + input: Value{ + Before: []interface{}{}, + After: nil, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateChange: change.ValidateSet(nil, plans.Delete, false), + }, + "set_delete_populated": { + input: Value{ + Before: []interface{}{"one", "two"}, + After: nil, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateChange: change.ValidateSet([]change.ValidateChangeFunc{ + change.ValidatePrimitive(strptr("\"one\""), nil, plans.Delete, false), + change.ValidatePrimitive(strptr("\"two\""), nil, plans.Delete, false), + }, plans.Delete, false), + }, + "set_create_sensitive": { + input: Value{ + Before: nil, + After: []interface{}{}, + AfterSensitive: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateChange: change.ValidateSensitive(nil, []interface{}{}, false, true, plans.Create, false), + }, + "set_update_sensitive": { + input: Value{ + Before: []interface{}{"one"}, + BeforeSensitive: true, + After: []interface{}{}, + AfterSensitive: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateChange: change.ValidateSensitive([]interface{}{"one"}, []interface{}{}, true, true, plans.Update, false), + }, + "set_delete_sensitive": { + input: Value{ + Before: []interface{}{}, + BeforeSensitive: true, + After: nil, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateChange: change.ValidateSensitive([]interface{}{}, nil, true, false, plans.Delete, false), + }, + "set_create_unknown": { + input: Value{ + Before: nil, + After: []interface{}{}, + Unknown: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateChange: change.ValidateComputed(nil, plans.Create, false), + }, + "set_update_unknown": { + input: Value{ + Before: []interface{}{}, + After: []interface{}{"one"}, + Unknown: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateChange: change.ValidateComputed(change.ValidateSet(nil, plans.Delete, false), plans.Update, false), + }, } for name, tc := range tcs {