mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Add support for lists in the structured renderer (#32401)
* prep for processing the structured run output * undo unwanted change to a json key * Add skeleton functions and API for refactored renderer * goimports * Fix documentation of the RenderOpts struct * Add rendering functionality for primitives to the structured renderer * add test case for override * Add support for parsing and rendering sensitive values in the renderer * Add support for unknown/computed values in the structured renderer * delete missing unit tests * Add support for object attributes in the structured renderer * goimports * Add support for the replace paths data in the structured renderer * Add support for maps in the structured renderer * Add support for lists in the structured renderer * goimports * add additional comments explaining
This commit is contained in:
parent
8975eebf84
commit
aff94591c1
120
internal/command/jsonformat/change/list.go
Normal file
120
internal/command/jsonformat/change/list.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package change
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/internal/command/format"
|
||||||
|
"github.com/hashicorp/terraform/internal/plans"
|
||||||
|
)
|
||||||
|
|
||||||
|
func List(elements []Change) Renderer {
|
||||||
|
return &listRenderer{
|
||||||
|
displayContext: true,
|
||||||
|
elements: elements,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NestedList(elements []Change) Renderer {
|
||||||
|
return &listRenderer{
|
||||||
|
elements: elements,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (renderer listRenderer) 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
|
||||||
|
|
||||||
|
unchangedElementOpts := opts.Clone()
|
||||||
|
unchangedElementOpts.showUnchangedChildren = true
|
||||||
|
|
||||||
|
var unchangedElements []Change
|
||||||
|
|
||||||
|
// renderNext tells the renderer to print out the next element in the list
|
||||||
|
// whatever state it is in. So, even if a change is a NoOp we will still
|
||||||
|
// print it out if the last change we processed wants us to.
|
||||||
|
renderNext := false
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString(fmt.Sprintf("[%s\n", change.forcesReplacement()))
|
||||||
|
for _, element := range renderer.elements {
|
||||||
|
if element.action == plans.NoOp && !renderNext && !opts.showUnchangedChildren {
|
||||||
|
unchangedElements = append(unchangedElements, element)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
renderNext = false
|
||||||
|
|
||||||
|
// If we want to display the context around this change, we want to
|
||||||
|
// render the change immediately before this change in the list, and the
|
||||||
|
// change immediately after in the list, even if both these changes are
|
||||||
|
// NoOps. This will give the user reading the diff some context as to
|
||||||
|
// where in the list these changes are being made, as order matters.
|
||||||
|
if renderer.displayContext {
|
||||||
|
// If our list of unchanged elements contains more than one entry
|
||||||
|
// we'll print out a count of the number of unchanged elements that
|
||||||
|
// we skipped. Note, this is the length of the unchanged elements
|
||||||
|
// minus 1 as the most recent unchanged element will be printed out
|
||||||
|
// in full.
|
||||||
|
if len(unchangedElements) > 1 {
|
||||||
|
buf.WriteString(fmt.Sprintf("%s%s %s\n", change.indent(indent+1), change.emptySymbol(), change.unchanged("element", len(unchangedElements)-1)))
|
||||||
|
}
|
||||||
|
// If our list of unchanged elements contains at least one entry,
|
||||||
|
// we're going to print out the most recent change in full. That's
|
||||||
|
// what happens here.
|
||||||
|
if len(unchangedElements) > 0 {
|
||||||
|
lastElement := unchangedElements[len(unchangedElements)-1]
|
||||||
|
buf.WriteString(fmt.Sprintf("%s %s,\n", change.indent(indent+1), lastElement.Render(indent+1, unchangedElementOpts)))
|
||||||
|
}
|
||||||
|
// We now reset the unchanged elements list, we've printed out a
|
||||||
|
// count of all the elements we skipped so we start counting from
|
||||||
|
// scratch again. This means that if we process a run of changed
|
||||||
|
// elements, they won't all start printing out summaries of every
|
||||||
|
// change that happened previously.
|
||||||
|
unchangedElements = nil
|
||||||
|
|
||||||
|
// As we also want to render the element immediately after any
|
||||||
|
// changes, we make a note here to say we should render the next
|
||||||
|
// change whatever it is. But, we only want to render the next
|
||||||
|
// change if the current change isn't a NoOp. If the current change
|
||||||
|
// is a NoOp then it was told to print by the last change and we
|
||||||
|
// don't want to cascade and print all changes from now on.
|
||||||
|
renderNext = element.action != plans.NoOp
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, warning := range element.Warnings(indent + 1) {
|
||||||
|
buf.WriteString(fmt.Sprintf("%s%s\n", change.indent(indent+1), warning))
|
||||||
|
}
|
||||||
|
if element.action == plans.NoOp {
|
||||||
|
buf.WriteString(fmt.Sprintf("%s %s,\n", change.indent(indent+1), element.Render(indent+1, unchangedElementOpts)))
|
||||||
|
} else {
|
||||||
|
buf.WriteString(fmt.Sprintf("%s%s %s,\n", change.indent(indent+1), format.DiffActionSymbol(element.action), element.Render(indent+1, elementOpts)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we were not displaying any context alongside our changes then the
|
||||||
|
// unchangedElements list will contain every unchanged element, and we'll
|
||||||
|
// print that out as we do with every other collection.
|
||||||
|
//
|
||||||
|
// If we were displaying context, then this will contain any unchanged
|
||||||
|
// elements since our last change, so we should also print it out.
|
||||||
|
if len(unchangedElements) > 0 {
|
||||||
|
buf.WriteString(fmt.Sprintf("%s%s %s\n", change.indent(indent+1), change.emptySymbol(), change.unchanged("element", len(unchangedElements))))
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString(fmt.Sprintf("%s%s ]%s", change.indent(indent), change.emptySymbol(), change.nullSuffix(opts.overrideNullSuffix)))
|
||||||
|
return buf.String()
|
||||||
|
}
|
@ -518,6 +518,24 @@ func TestRenderers(t *testing.T) {
|
|||||||
{
|
{
|
||||||
~ "element_one" = (sensitive)
|
~ "element_one" = (sensitive)
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"map_update_sensitive_element_status": {
|
||||||
|
change: Change{
|
||||||
|
renderer: Map(map[string]Change{
|
||||||
|
"element_one": {
|
||||||
|
renderer: Sensitive(0, 0, true, false),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
{
|
||||||
|
# Warning: this attribute value will no longer be marked as sensitive
|
||||||
|
# after applying this change. The value is unchanged.
|
||||||
|
~ "element_one" = (sensitive)
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
"map_delete_sensitive_element": {
|
"map_delete_sensitive_element": {
|
||||||
@ -569,6 +587,308 @@ func TestRenderers(t *testing.T) {
|
|||||||
{
|
{
|
||||||
~ "element_one" = 1 -> (known after apply)
|
~ "element_one" = 1 -> (known after apply)
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_create_empty": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{}),
|
||||||
|
action: plans.Create,
|
||||||
|
},
|
||||||
|
expected: "[]",
|
||||||
|
},
|
||||||
|
"list_create": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{
|
||||||
|
{
|
||||||
|
renderer: Primitive(nil, strptr("1")),
|
||||||
|
action: plans.Create,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
action: plans.Create,
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[
|
||||||
|
+ 1,
|
||||||
|
]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_delete_empty": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{}),
|
||||||
|
action: plans.Delete,
|
||||||
|
},
|
||||||
|
expected: "[] -> null",
|
||||||
|
},
|
||||||
|
"list_delete": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{
|
||||||
|
{
|
||||||
|
renderer: Primitive(strptr("1"), nil),
|
||||||
|
action: plans.Delete,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
action: plans.Delete,
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[
|
||||||
|
- 1,
|
||||||
|
] -> null
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_create_element": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{
|
||||||
|
{
|
||||||
|
renderer: Primitive(nil, strptr("1")),
|
||||||
|
action: plans.Create,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[
|
||||||
|
+ 1,
|
||||||
|
]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_update_element": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{
|
||||||
|
{
|
||||||
|
renderer: Primitive(strptr("0"), strptr("1")),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[
|
||||||
|
~ 0 -> 1,
|
||||||
|
]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_replace_element": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{
|
||||||
|
{
|
||||||
|
renderer: Primitive(strptr("0"), nil),
|
||||||
|
action: plans.Delete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
renderer: Primitive(nil, strptr("1")),
|
||||||
|
action: plans.Create,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[
|
||||||
|
- 0,
|
||||||
|
+ 1,
|
||||||
|
]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_delete_element": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{
|
||||||
|
{
|
||||||
|
renderer: Primitive(strptr("0"), nil),
|
||||||
|
action: plans.Delete,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[
|
||||||
|
- 0,
|
||||||
|
]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_update_forces_replacement": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{
|
||||||
|
{
|
||||||
|
renderer: Primitive(strptr("0"), strptr("1")),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
action: plans.Update,
|
||||||
|
replace: true,
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[ # forces replacement
|
||||||
|
~ 0 -> 1,
|
||||||
|
]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_update_ignores_unchanged": {
|
||||||
|
change: Change{
|
||||||
|
renderer: NestedList([]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)
|
||||||
|
]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_update_ignored_unchanged_with_context": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]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: `
|
||||||
|
[
|
||||||
|
# (1 unchanged element hidden)
|
||||||
|
1,
|
||||||
|
~ 2 -> 5,
|
||||||
|
3,
|
||||||
|
# (1 unchanged element hidden)
|
||||||
|
]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_create_sensitive_element": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{
|
||||||
|
{
|
||||||
|
renderer: Sensitive(nil, 1, false, true),
|
||||||
|
action: plans.Create,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[
|
||||||
|
+ (sensitive),
|
||||||
|
]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_delete_sensitive_element": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{
|
||||||
|
{
|
||||||
|
renderer: Sensitive(1, nil, true, false),
|
||||||
|
action: plans.Delete,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[
|
||||||
|
- (sensitive),
|
||||||
|
]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_update_sensitive_element": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{
|
||||||
|
{
|
||||||
|
renderer: Sensitive(nil, 1, false, true),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[
|
||||||
|
~ (sensitive),
|
||||||
|
]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_update_sensitive_element_status": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{
|
||||||
|
{
|
||||||
|
renderer: Sensitive(1, 1, 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. The value is unchanged.
|
||||||
|
~ (sensitive),
|
||||||
|
]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_create_computed_element": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{
|
||||||
|
{
|
||||||
|
renderer: Computed(Change{}),
|
||||||
|
action: plans.Create,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[
|
||||||
|
+ (known after apply),
|
||||||
|
]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"list_update_computed_element": {
|
||||||
|
change: Change{
|
||||||
|
renderer: List([]Change{
|
||||||
|
{
|
||||||
|
renderer: Computed(Change{
|
||||||
|
renderer: Primitive(strptr("0"), nil),
|
||||||
|
action: plans.Delete,
|
||||||
|
}),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
action: plans.Update,
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[
|
||||||
|
~ 0 -> (known after apply),
|
||||||
|
]
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -126,6 +126,50 @@ func ValidateMap(elements map[string]ValidateChangeFunc, action plans.Action, re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ValidateList(elements []ValidateChangeFunc, action plans.Action, replace bool) ValidateChangeFunc {
|
||||||
|
return func(t *testing.T, change Change) {
|
||||||
|
validateChange(t, change, action, replace)
|
||||||
|
|
||||||
|
list, ok := change.renderer.(*listRenderer)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("invalid renderer type: %T", change.renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !list.displayContext {
|
||||||
|
t.Fatalf("created the wrong type of list renderer")
|
||||||
|
}
|
||||||
|
|
||||||
|
validateList(t, list, elements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateNestedList(elements []ValidateChangeFunc, action plans.Action, replace bool) ValidateChangeFunc {
|
||||||
|
return func(t *testing.T, change Change) {
|
||||||
|
validateChange(t, change, action, replace)
|
||||||
|
|
||||||
|
list, ok := change.renderer.(*listRenderer)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("invalid renderer type: %T", change.renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if list.displayContext {
|
||||||
|
t.Fatalf("created the wrong type of list renderer")
|
||||||
|
}
|
||||||
|
|
||||||
|
validateList(t, list, elements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateList(t *testing.T, list *listRenderer, elements []ValidateChangeFunc) {
|
||||||
|
if len(list.elements) != len(elements) {
|
||||||
|
t.Fatalf("expected %d elements but found %d elements", len(elements), len(list.elements))
|
||||||
|
}
|
||||||
|
|
||||||
|
for ix := 0; ix < len(elements); ix++ {
|
||||||
|
elements[ix](t, list.elements[ix])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ValidateSensitive(before, after interface{}, beforeSensitive, afterSensitive bool, action plans.Action, replace bool) ValidateChangeFunc {
|
func ValidateSensitive(before, after interface{}, beforeSensitive, afterSensitive bool, action plans.Action, replace bool) ValidateChangeFunc {
|
||||||
return func(t *testing.T, change Change) {
|
return func(t *testing.T, change Change) {
|
||||||
validateChange(t, change, action, replace)
|
validateChange(t, change, action, replace)
|
||||||
|
@ -21,6 +21,8 @@ func (v Value) computeChangeForNestedAttribute(attribute *jsonprovider.NestedTyp
|
|||||||
return v.computeAttributeChangeAsNestedObject(attribute.Attributes)
|
return v.computeAttributeChangeAsNestedObject(attribute.Attributes)
|
||||||
case "map":
|
case "map":
|
||||||
return v.computeAttributeChangeAsNestedMap(attribute.Attributes)
|
return v.computeAttributeChangeAsNestedMap(attribute.Attributes)
|
||||||
|
case "list":
|
||||||
|
return v.computeAttributeChangeAsNestedList(attribute.Attributes)
|
||||||
default:
|
default:
|
||||||
panic("unrecognized nesting mode: " + attribute.NestingMode)
|
panic("unrecognized nesting mode: " + attribute.NestingMode)
|
||||||
}
|
}
|
||||||
@ -34,8 +36,10 @@ func (v Value) computeChangeForType(ctyType cty.Type) change.Change {
|
|||||||
return v.computeAttributeChangeAsObject(ctyType.AttributeTypes())
|
return v.computeAttributeChangeAsObject(ctyType.AttributeTypes())
|
||||||
case ctyType.IsMapType():
|
case ctyType.IsMapType():
|
||||||
return v.computeAttributeChangeAsMap(ctyType.ElementType())
|
return v.computeAttributeChangeAsMap(ctyType.ElementType())
|
||||||
|
case ctyType.IsListType():
|
||||||
|
return v.computeAttributeChangeAsList(ctyType.ElementType())
|
||||||
default:
|
default:
|
||||||
panic("not implemented")
|
panic("unrecognized type: " + ctyType.FriendlyName())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
165
internal/command/jsonformat/differ/list.go
Normal file
165
internal/command/jsonformat/differ/list.go
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
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) computeAttributeChangeAsList(elementType cty.Type) change.Change {
|
||||||
|
var elements []change.Change
|
||||||
|
current := v.getDefaultActionForIteration()
|
||||||
|
v.processList(elementType, func(value Value) {
|
||||||
|
element := value.ComputeChange(elementType)
|
||||||
|
elements = append(elements, element)
|
||||||
|
current = compareActions(current, element.GetAction())
|
||||||
|
})
|
||||||
|
return change.New(change.List(elements), current, v.replacePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Value) computeAttributeChangeAsNestedList(attributes map[string]*jsonprovider.Attribute) change.Change {
|
||||||
|
var elements []change.Change
|
||||||
|
current := v.getDefaultActionForIteration()
|
||||||
|
v.processNestedList(func(value Value) {
|
||||||
|
element := value.ComputeChange(attributes)
|
||||||
|
elements = append(elements, element)
|
||||||
|
current = compareActions(current, element.GetAction())
|
||||||
|
})
|
||||||
|
return change.New(change.NestedList(elements), current, v.replacePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Value) processNestedList(process func(value Value)) {
|
||||||
|
sliceValue := v.asSlice()
|
||||||
|
for ix := 0; ix < len(sliceValue.Before) || ix < len(sliceValue.After); ix++ {
|
||||||
|
process(sliceValue.getChild(ix, ix, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Value) processList(elementType cty.Type, process func(value Value)) {
|
||||||
|
sliceValue := v.asSlice()
|
||||||
|
|
||||||
|
lcs := lcs(sliceValue.Before, sliceValue.After)
|
||||||
|
var beforeIx, afterIx, lcsIx int
|
||||||
|
for beforeIx < len(sliceValue.Before) || afterIx < len(sliceValue.After) || lcsIx < len(lcs) {
|
||||||
|
// Step through all the before values until we hit the next item in the
|
||||||
|
// longest common subsequence. We are going to just say that all of
|
||||||
|
// these have been deleted.
|
||||||
|
for beforeIx < len(sliceValue.Before) && (lcsIx >= len(lcs) || !reflect.DeepEqual(sliceValue.Before[beforeIx], lcs[lcsIx])) {
|
||||||
|
isObjectDiff := elementType.IsObjectType() && afterIx < len(sliceValue.After) && (lcsIx >= len(lcs) || !reflect.DeepEqual(sliceValue.After[afterIx], lcs[lcsIx]))
|
||||||
|
if isObjectDiff {
|
||||||
|
process(sliceValue.getChild(beforeIx, afterIx, false))
|
||||||
|
beforeIx++
|
||||||
|
afterIx++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
process(sliceValue.getChild(beforeIx, len(sliceValue.After), false))
|
||||||
|
beforeIx++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, step through all the after values until hit the next item in the
|
||||||
|
// LCS. We are going to say that all of these have been created.
|
||||||
|
for afterIx < len(sliceValue.After) && (lcsIx >= len(lcs) || !reflect.DeepEqual(sliceValue.After[afterIx], lcs[lcsIx])) {
|
||||||
|
process(sliceValue.getChild(len(sliceValue.Before), afterIx, false))
|
||||||
|
afterIx++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, add the item in common as unchanged.
|
||||||
|
if lcsIx < len(lcs) {
|
||||||
|
process(sliceValue.getChild(beforeIx, afterIx, false))
|
||||||
|
beforeIx++
|
||||||
|
afterIx++
|
||||||
|
lcsIx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lcs(xs, ys []interface{}) []interface{} {
|
||||||
|
if len(xs) == 0 || len(ys) == 0 {
|
||||||
|
return make([]interface{}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := make([]int, len(xs)*len(ys))
|
||||||
|
eqs := make([]bool, len(xs)*len(ys))
|
||||||
|
w := len(xs)
|
||||||
|
|
||||||
|
for y := 0; y < len(ys); y++ {
|
||||||
|
for x := 0; x < len(xs); x++ {
|
||||||
|
eq := false
|
||||||
|
if reflect.DeepEqual(xs[x], ys[y]) {
|
||||||
|
eq = true
|
||||||
|
eqs[(w*y)+x] = true // equality tests can be expensive, so cache it
|
||||||
|
}
|
||||||
|
if eq {
|
||||||
|
// Sequence gets one longer than for the cell at top left,
|
||||||
|
// since we'd append a new item to the sequence here.
|
||||||
|
if x == 0 || y == 0 {
|
||||||
|
c[(w*y)+x] = 1
|
||||||
|
} else {
|
||||||
|
c[(w*y)+x] = c[(w*(y-1))+(x-1)] + 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We follow the longest of the sequence above and the sequence
|
||||||
|
// to the left of us in the matrix.
|
||||||
|
l := 0
|
||||||
|
u := 0
|
||||||
|
if x > 0 {
|
||||||
|
l = c[(w*y)+(x-1)]
|
||||||
|
}
|
||||||
|
if y > 0 {
|
||||||
|
u = c[(w*(y-1))+x]
|
||||||
|
}
|
||||||
|
if l > u {
|
||||||
|
c[(w*y)+x] = l
|
||||||
|
} else {
|
||||||
|
c[(w*y)+x] = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The bottom right cell tells us how long our longest sequence will be
|
||||||
|
seq := make([]interface{}, c[len(c)-1])
|
||||||
|
|
||||||
|
// Now we will walk back from the bottom right cell, finding again all
|
||||||
|
// of the equal pairs to construct our sequence.
|
||||||
|
x := len(xs) - 1
|
||||||
|
y := len(ys) - 1
|
||||||
|
i := len(seq) - 1
|
||||||
|
|
||||||
|
for x > -1 && y > -1 {
|
||||||
|
if eqs[(w*y)+x] {
|
||||||
|
// Add the value to our result list and then walk diagonally
|
||||||
|
// up and to the left.
|
||||||
|
seq[i] = xs[x]
|
||||||
|
x--
|
||||||
|
y--
|
||||||
|
i--
|
||||||
|
} else {
|
||||||
|
// Take the path with the greatest sequence length in the matrix.
|
||||||
|
l := 0
|
||||||
|
u := 0
|
||||||
|
if x > 0 {
|
||||||
|
l = c[(w*y)+(x-1)]
|
||||||
|
}
|
||||||
|
if y > 0 {
|
||||||
|
u = c[(w*(y-1))+x]
|
||||||
|
}
|
||||||
|
if l > u {
|
||||||
|
x--
|
||||||
|
} else {
|
||||||
|
y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i > -1 {
|
||||||
|
// should never happen if the matrix was constructed properly
|
||||||
|
panic("not enough elements in sequence")
|
||||||
|
}
|
||||||
|
|
||||||
|
return seq
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
package differ
|
package differ
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/internal/command/jsonformat/change"
|
"github.com/hashicorp/terraform/internal/command/jsonformat/change"
|
||||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (v Value) computeAttributeChangeAsMap(elementType cty.Type) change.Change {
|
func (v Value) computeAttributeChangeAsMap(elementType cty.Type) change.Change {
|
||||||
|
@ -61,6 +61,9 @@ func (m ValueMap) processReplacePaths(key string) []interface{} {
|
|||||||
path := p.([]interface{})
|
path := p.([]interface{})
|
||||||
|
|
||||||
if len(path) == 0 {
|
if len(path) == 0 {
|
||||||
|
// This means that the current value is causing a replacement but
|
||||||
|
// not its children, so we skip as we are returning the child's
|
||||||
|
// value.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
96
internal/command/jsonformat/differ/value_slice.go
Normal file
96
internal/command/jsonformat/differ/value_slice.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package differ
|
||||||
|
|
||||||
|
type ValueSlice struct {
|
||||||
|
// Before contains the value before the proposed change.
|
||||||
|
Before []interface{}
|
||||||
|
|
||||||
|
// After contains the value after the proposed change.
|
||||||
|
After []interface{}
|
||||||
|
|
||||||
|
// Unknown contains the unknown status of any elements of this list/set.
|
||||||
|
Unknown []interface{}
|
||||||
|
|
||||||
|
// BeforeSensitive contains the before sensitive status of any elements of
|
||||||
|
//this list/set.
|
||||||
|
BeforeSensitive []interface{}
|
||||||
|
|
||||||
|
// AfterSensitive contains the after sensitive status of any elements of
|
||||||
|
//this list/set.
|
||||||
|
AfterSensitive []interface{}
|
||||||
|
|
||||||
|
// ReplacePaths matches the same attributes in Value exactly.
|
||||||
|
ReplacePaths []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Value) asSlice() ValueSlice {
|
||||||
|
return ValueSlice{
|
||||||
|
Before: genericToSlice(v.Before),
|
||||||
|
After: genericToSlice(v.After),
|
||||||
|
Unknown: genericToSlice(v.Unknown),
|
||||||
|
BeforeSensitive: genericToSlice(v.BeforeSensitive),
|
||||||
|
AfterSensitive: genericToSlice(v.AfterSensitive),
|
||||||
|
ReplacePaths: v.ReplacePaths,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ValueSlice) getChild(beforeIx, afterIx int, propagateReplace bool) Value {
|
||||||
|
before, beforeExplicit := getFromGenericSlice(s.Before, beforeIx)
|
||||||
|
after, afterExplicit := getFromGenericSlice(s.After, afterIx)
|
||||||
|
unknown, _ := getFromGenericSlice(s.Unknown, afterIx)
|
||||||
|
beforeSensitive, _ := getFromGenericSlice(s.BeforeSensitive, beforeIx)
|
||||||
|
afterSensitive, _ := getFromGenericSlice(s.AfterSensitive, afterIx)
|
||||||
|
|
||||||
|
return Value{
|
||||||
|
BeforeExplicit: beforeExplicit,
|
||||||
|
AfterExplicit: afterExplicit,
|
||||||
|
Before: before,
|
||||||
|
After: after,
|
||||||
|
Unknown: unknown,
|
||||||
|
BeforeSensitive: beforeSensitive,
|
||||||
|
AfterSensitive: afterSensitive,
|
||||||
|
ReplacePaths: s.processReplacePaths(beforeIx, propagateReplace),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ValueSlice) processReplacePaths(ix int, propagateReplace bool) []interface{} {
|
||||||
|
var ret []interface{}
|
||||||
|
for _, p := range s.ReplacePaths {
|
||||||
|
path := p.([]interface{})
|
||||||
|
|
||||||
|
if len(path) == 0 {
|
||||||
|
// This means that the current value is causing a replacement but
|
||||||
|
// not its children. Normally, we'd skip this as we do with maps
|
||||||
|
// but sets display the replace suffix on all their children even
|
||||||
|
// if they themselves are specified, so we want to pass this on.
|
||||||
|
if propagateReplace {
|
||||||
|
ret = append(ret, path)
|
||||||
|
}
|
||||||
|
// If we don't want to propagate the replace we just skip over this
|
||||||
|
// entry. If we do, we've added it to the returned set of paths
|
||||||
|
// already, so we still want to skip over the rest of this.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(path[0].(float64)) == ix {
|
||||||
|
ret = append(ret, path[1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFromGenericSlice(generic []interface{}, ix int) (interface{}, bool) {
|
||||||
|
if generic == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if ix < 0 || ix >= len(generic) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return generic[ix], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func genericToSlice(generic interface{}) []interface{} {
|
||||||
|
if concrete, ok := generic.([]interface{}); ok {
|
||||||
|
return concrete
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -422,6 +422,34 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
|||||||
validate(t, input.ComputeChange(attribute))
|
validate(t, input.ComputeChange(attribute))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("list", func(t *testing.T) {
|
||||||
|
attribute := &jsonprovider.Attribute{
|
||||||
|
AttributeType: unmarshalType(t, cty.List(cty.Object(tc.attributes))),
|
||||||
|
}
|
||||||
|
|
||||||
|
input := wrapValueInSlice(tc.input)
|
||||||
|
|
||||||
|
if tc.validateObject != nil {
|
||||||
|
validate := change.ValidateList([]change.ValidateChangeFunc{
|
||||||
|
tc.validateObject,
|
||||||
|
}, collectionDefaultAction, false)
|
||||||
|
validate(t, input.ComputeChange(attribute))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.validateSingleChange != nil {
|
||||||
|
validate := change.ValidateList([]change.ValidateChangeFunc{
|
||||||
|
tc.validateSingleChange,
|
||||||
|
}, collectionDefaultAction, false)
|
||||||
|
validate(t, input.ComputeChange(attribute))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validate := change.ValidateList([]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) {
|
t.Run(fmt.Sprintf("nested_%s", name), func(t *testing.T) {
|
||||||
@ -494,6 +522,46 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
|||||||
}, collectionDefaultAction, false)
|
}, collectionDefaultAction, false)
|
||||||
validate(t, input.ComputeChange(attribute))
|
validate(t, input.ComputeChange(attribute))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("list", 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: "list",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
input := wrapValueInSlice(tc.input)
|
||||||
|
|
||||||
|
if tc.validateNestedObject != nil {
|
||||||
|
validate := change.ValidateNestedList([]change.ValidateChangeFunc{
|
||||||
|
tc.validateNestedObject,
|
||||||
|
}, collectionDefaultAction, false)
|
||||||
|
validate(t, input.ComputeChange(attribute))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.validateSingleChange != nil {
|
||||||
|
validate := change.ValidateNestedList([]change.ValidateChangeFunc{
|
||||||
|
tc.validateSingleChange,
|
||||||
|
}, collectionDefaultAction, false)
|
||||||
|
validate(t, input.ComputeChange(attribute))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validate := change.ValidateNestedList([]change.ValidateChangeFunc{
|
||||||
|
change.ValidateNestedObject(tc.validateChanges, tc.validateAction, tc.validateReplace),
|
||||||
|
}, collectionDefaultAction, false)
|
||||||
|
validate(t, input.ComputeChange(attribute))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -504,9 +572,10 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
|
|||||||
// contexts of collections.
|
// contexts of collections.
|
||||||
|
|
||||||
tcs := map[string]struct {
|
tcs := map[string]struct {
|
||||||
input Value
|
input Value
|
||||||
attribute cty.Type
|
attribute cty.Type
|
||||||
validateChange change.ValidateChangeFunc
|
validateChange change.ValidateChangeFunc
|
||||||
|
validateListChanges []change.ValidateChangeFunc // Lists are special in some cases.
|
||||||
}{
|
}{
|
||||||
"primitive_create": {
|
"primitive_create": {
|
||||||
input: Value{
|
input: Value{
|
||||||
@ -529,6 +598,10 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
attribute: cty.String,
|
attribute: cty.String,
|
||||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, false),
|
validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, false),
|
||||||
|
validateListChanges: []change.ValidateChangeFunc{
|
||||||
|
change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||||
|
change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"primitive_set_explicit_null": {
|
"primitive_set_explicit_null": {
|
||||||
input: Value{
|
input: Value{
|
||||||
@ -538,6 +611,10 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
attribute: cty.String,
|
attribute: cty.String,
|
||||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), nil, plans.Update, false),
|
validateChange: change.ValidatePrimitive(strptr("\"old\""), nil, plans.Update, false),
|
||||||
|
validateListChanges: []change.ValidateChangeFunc{
|
||||||
|
change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||||
|
change.ValidatePrimitive(nil, nil, plans.Create, false),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"primitive_unset_explicit_null": {
|
"primitive_unset_explicit_null": {
|
||||||
input: Value{
|
input: Value{
|
||||||
@ -547,6 +624,10 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
attribute: cty.String,
|
attribute: cty.String,
|
||||||
validateChange: change.ValidatePrimitive(nil, strptr("\"new\""), plans.Update, false),
|
validateChange: change.ValidatePrimitive(nil, strptr("\"new\""), plans.Update, false),
|
||||||
|
validateListChanges: []change.ValidateChangeFunc{
|
||||||
|
change.ValidatePrimitive(nil, nil, plans.Delete, false),
|
||||||
|
change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"primitive_create_sensitive": {
|
"primitive_create_sensitive": {
|
||||||
input: Value{
|
input: Value{
|
||||||
@ -575,6 +656,10 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
attribute: cty.String,
|
attribute: cty.String,
|
||||||
validateChange: change.ValidateSensitive("old", "new", true, true, plans.Update, false),
|
validateChange: change.ValidateSensitive("old", "new", true, true, plans.Update, false),
|
||||||
|
validateListChanges: []change.ValidateChangeFunc{
|
||||||
|
change.ValidateSensitive("old", nil, true, false, plans.Delete, false),
|
||||||
|
change.ValidateSensitive(nil, "new", false, true, plans.Create, false),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"primitive_create_computed": {
|
"primitive_create_computed": {
|
||||||
input: Value{
|
input: Value{
|
||||||
@ -593,6 +678,10 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
attribute: cty.String,
|
attribute: cty.String,
|
||||||
validateChange: change.ValidateComputed(change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), plans.Update, false),
|
validateChange: change.ValidateComputed(change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), plans.Update, false),
|
||||||
|
validateListChanges: []change.ValidateChangeFunc{
|
||||||
|
change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||||
|
change.ValidateComputed(nil, plans.Create, false),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"primitive_update_replace": {
|
"primitive_update_replace": {
|
||||||
input: Value{
|
input: Value{
|
||||||
@ -604,6 +693,10 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
attribute: cty.String,
|
attribute: cty.String,
|
||||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, true),
|
validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, true),
|
||||||
|
validateListChanges: []change.ValidateChangeFunc{
|
||||||
|
change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, true),
|
||||||
|
change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"noop": {
|
"noop": {
|
||||||
input: Value{
|
input: Value{
|
||||||
@ -640,6 +733,24 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
|
|||||||
}, defaultCollectionsAction, false)
|
}, defaultCollectionsAction, false)
|
||||||
validate(t, input.ComputeChange(attribute))
|
validate(t, input.ComputeChange(attribute))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("list", func(t *testing.T) {
|
||||||
|
input := wrapValueInSlice(tc.input)
|
||||||
|
attribute := &jsonprovider.Attribute{
|
||||||
|
AttributeType: unmarshalType(t, cty.List(tc.attribute)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.validateListChanges != nil {
|
||||||
|
validate := change.ValidateList(tc.validateListChanges, defaultCollectionsAction, false)
|
||||||
|
validate(t, input.ComputeChange(attribute))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validate := change.ValidateList([]change.ValidateChangeFunc{
|
||||||
|
tc.validateChange,
|
||||||
|
}, defaultCollectionsAction, false)
|
||||||
|
validate(t, input.ComputeChange(attribute))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -765,6 +876,108 @@ func TestValue_CollectionAttributes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
validateChange: change.ValidateComputed(change.ValidateMap(nil, plans.Delete, false), plans.Update, false),
|
validateChange: change.ValidateComputed(change.ValidateMap(nil, plans.Delete, false), plans.Update, false),
|
||||||
},
|
},
|
||||||
|
"list_create_empty": {
|
||||||
|
input: Value{
|
||||||
|
Before: nil,
|
||||||
|
After: []interface{}{},
|
||||||
|
},
|
||||||
|
attribute: &jsonprovider.Attribute{
|
||||||
|
AttributeType: unmarshalType(t, cty.List(cty.String)),
|
||||||
|
},
|
||||||
|
validateChange: change.ValidateList(nil, plans.Create, false),
|
||||||
|
},
|
||||||
|
"list_create_populated": {
|
||||||
|
input: Value{
|
||||||
|
Before: nil,
|
||||||
|
After: []interface{}{"one", "two"},
|
||||||
|
},
|
||||||
|
attribute: &jsonprovider.Attribute{
|
||||||
|
AttributeType: unmarshalType(t, cty.List(cty.String)),
|
||||||
|
},
|
||||||
|
validateChange: change.ValidateList([]change.ValidateChangeFunc{
|
||||||
|
change.ValidatePrimitive(nil, strptr("\"one\""), plans.Create, false),
|
||||||
|
change.ValidatePrimitive(nil, strptr("\"two\""), plans.Create, false),
|
||||||
|
}, plans.Create, false),
|
||||||
|
},
|
||||||
|
"list_delete_empty": {
|
||||||
|
input: Value{
|
||||||
|
Before: []interface{}{},
|
||||||
|
After: nil,
|
||||||
|
},
|
||||||
|
attribute: &jsonprovider.Attribute{
|
||||||
|
AttributeType: unmarshalType(t, cty.List(cty.String)),
|
||||||
|
},
|
||||||
|
validateChange: change.ValidateList(nil, plans.Delete, false),
|
||||||
|
},
|
||||||
|
"list_delete_populated": {
|
||||||
|
input: Value{
|
||||||
|
Before: []interface{}{"one", "two"},
|
||||||
|
After: nil,
|
||||||
|
},
|
||||||
|
attribute: &jsonprovider.Attribute{
|
||||||
|
AttributeType: unmarshalType(t, cty.List(cty.String)),
|
||||||
|
},
|
||||||
|
validateChange: change.ValidateList([]change.ValidateChangeFunc{
|
||||||
|
change.ValidatePrimitive(strptr("\"one\""), nil, plans.Delete, false),
|
||||||
|
change.ValidatePrimitive(strptr("\"two\""), nil, plans.Delete, false),
|
||||||
|
}, plans.Delete, false),
|
||||||
|
},
|
||||||
|
"list_create_sensitive": {
|
||||||
|
input: Value{
|
||||||
|
Before: nil,
|
||||||
|
After: []interface{}{},
|
||||||
|
AfterSensitive: true,
|
||||||
|
},
|
||||||
|
attribute: &jsonprovider.Attribute{
|
||||||
|
AttributeType: unmarshalType(t, cty.List(cty.String)),
|
||||||
|
},
|
||||||
|
validateChange: change.ValidateSensitive(nil, []interface{}{}, false, true, plans.Create, false),
|
||||||
|
},
|
||||||
|
"list_update_sensitive": {
|
||||||
|
input: Value{
|
||||||
|
Before: []interface{}{"one"},
|
||||||
|
BeforeSensitive: true,
|
||||||
|
After: []interface{}{},
|
||||||
|
AfterSensitive: true,
|
||||||
|
},
|
||||||
|
attribute: &jsonprovider.Attribute{
|
||||||
|
AttributeType: unmarshalType(t, cty.List(cty.String)),
|
||||||
|
},
|
||||||
|
validateChange: change.ValidateSensitive([]interface{}{"one"}, []interface{}{}, true, true, plans.Update, false),
|
||||||
|
},
|
||||||
|
"list_delete_sensitive": {
|
||||||
|
input: Value{
|
||||||
|
Before: []interface{}{},
|
||||||
|
BeforeSensitive: true,
|
||||||
|
After: nil,
|
||||||
|
},
|
||||||
|
attribute: &jsonprovider.Attribute{
|
||||||
|
AttributeType: unmarshalType(t, cty.List(cty.String)),
|
||||||
|
},
|
||||||
|
validateChange: change.ValidateSensitive([]interface{}{}, nil, true, false, plans.Delete, false),
|
||||||
|
},
|
||||||
|
"list_create_unknown": {
|
||||||
|
input: Value{
|
||||||
|
Before: nil,
|
||||||
|
After: []interface{}{},
|
||||||
|
Unknown: true,
|
||||||
|
},
|
||||||
|
attribute: &jsonprovider.Attribute{
|
||||||
|
AttributeType: unmarshalType(t, cty.List(cty.String)),
|
||||||
|
},
|
||||||
|
validateChange: change.ValidateComputed(nil, plans.Create, false),
|
||||||
|
},
|
||||||
|
"list_update_unknown": {
|
||||||
|
input: Value{
|
||||||
|
Before: []interface{}{},
|
||||||
|
After: []interface{}{"one"},
|
||||||
|
Unknown: true,
|
||||||
|
},
|
||||||
|
attribute: &jsonprovider.Attribute{
|
||||||
|
AttributeType: unmarshalType(t, cty.List(cty.String)),
|
||||||
|
},
|
||||||
|
validateChange: change.ValidateComputed(change.ValidateList(nil, plans.Delete, false), plans.Update, false),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, tc := range tcs {
|
for name, tc := range tcs {
|
||||||
@ -785,10 +998,27 @@ func unmarshalType(t *testing.T, ctyType cty.Type) json.RawMessage {
|
|||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wrapValueInSlice does the same as wrapValueInMap, except it wraps it into a
|
||||||
|
// slice internally.
|
||||||
|
func wrapValueInSlice(input Value) Value {
|
||||||
|
return wrapValue(input, float64(0), func(value interface{}, unknown interface{}, explicit bool) interface{} {
|
||||||
|
switch value.(type) {
|
||||||
|
case nil:
|
||||||
|
if set, ok := unknown.(bool); (set && ok) || explicit {
|
||||||
|
return []interface{}{nil}
|
||||||
|
|
||||||
|
}
|
||||||
|
return []interface{}{}
|
||||||
|
default:
|
||||||
|
return []interface{}{value}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// wrapValueInMap access a single Value and returns a new Value that represents
|
// wrapValueInMap access a single Value and returns a new Value that represents
|
||||||
// a map with a single element. That single element is the input value.
|
// a map with a single element. That single element is the input value.
|
||||||
func wrapValueInMap(input Value) Value {
|
func wrapValueInMap(input Value) Value {
|
||||||
tomap := func(value interface{}, unknown interface{}, explicit bool) interface{} {
|
return wrapValue(input, "element", func(value interface{}, unknown interface{}, explicit bool) interface{} {
|
||||||
switch value.(type) {
|
switch value.(type) {
|
||||||
case nil:
|
case nil:
|
||||||
if set, ok := unknown.(bool); (set && ok) || explicit {
|
if set, ok := unknown.(bool); (set && ok) || explicit {
|
||||||
@ -802,20 +1032,22 @@ func wrapValueInMap(input Value) Value {
|
|||||||
"element": value,
|
"element": value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapValue(input Value, step interface{}, wrap func(interface{}, interface{}, bool) interface{}) Value {
|
||||||
return Value{
|
return Value{
|
||||||
Before: tomap(input.Before, nil, input.BeforeExplicit),
|
Before: wrap(input.Before, nil, input.BeforeExplicit),
|
||||||
After: tomap(input.After, input.Unknown, input.AfterExplicit),
|
After: wrap(input.After, input.Unknown, input.AfterExplicit),
|
||||||
Unknown: tomap(input.Unknown, nil, false),
|
Unknown: wrap(input.Unknown, nil, false),
|
||||||
BeforeSensitive: tomap(input.BeforeSensitive, nil, false),
|
BeforeSensitive: wrap(input.BeforeSensitive, nil, false),
|
||||||
AfterSensitive: tomap(input.AfterSensitive, nil, false),
|
AfterSensitive: wrap(input.AfterSensitive, nil, false),
|
||||||
ReplacePaths: func() []interface{} {
|
ReplacePaths: func() []interface{} {
|
||||||
var ret []interface{}
|
var ret []interface{}
|
||||||
for _, path := range input.ReplacePaths {
|
for _, path := range input.ReplacePaths {
|
||||||
old := path.([]interface{})
|
old := path.([]interface{})
|
||||||
var updated []interface{}
|
var updated []interface{}
|
||||||
updated = append(updated, "element")
|
updated = append(updated, step)
|
||||||
updated = append(updated, old...)
|
updated = append(updated, old...)
|
||||||
ret = append(ret, updated)
|
ret = append(ret, updated)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user