mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -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)
|
||||
}
|
||||
`,
|
||||
},
|
||||
"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": {
|
||||
@ -569,6 +587,308 @@ func TestRenderers(t *testing.T) {
|
||||
{
|
||||
~ "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 {
|
||||
return func(t *testing.T, change Change) {
|
||||
validateChange(t, change, action, replace)
|
||||
|
@ -21,6 +21,8 @@ func (v Value) computeChangeForNestedAttribute(attribute *jsonprovider.NestedTyp
|
||||
return v.computeAttributeChangeAsNestedObject(attribute.Attributes)
|
||||
case "map":
|
||||
return v.computeAttributeChangeAsNestedMap(attribute.Attributes)
|
||||
case "list":
|
||||
return v.computeAttributeChangeAsNestedList(attribute.Attributes)
|
||||
default:
|
||||
panic("unrecognized nesting mode: " + attribute.NestingMode)
|
||||
}
|
||||
@ -34,8 +36,10 @@ func (v Value) computeChangeForType(ctyType cty.Type) change.Change {
|
||||
return v.computeAttributeChangeAsObject(ctyType.AttributeTypes())
|
||||
case ctyType.IsMapType():
|
||||
return v.computeAttributeChangeAsMap(ctyType.ElementType())
|
||||
case ctyType.IsListType():
|
||||
return v.computeAttributeChangeAsList(ctyType.ElementType())
|
||||
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
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/change"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func (v Value) computeAttributeChangeAsMap(elementType cty.Type) change.Change {
|
||||
|
@ -61,6 +61,9 @@ func (m ValueMap) processReplacePaths(key string) []interface{} {
|
||||
path := p.([]interface{})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
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))
|
||||
})
|
||||
|
||||
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) {
|
||||
@ -494,6 +522,46 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
}, collectionDefaultAction, false)
|
||||
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.
|
||||
|
||||
tcs := map[string]struct {
|
||||
input Value
|
||||
attribute cty.Type
|
||||
validateChange change.ValidateChangeFunc
|
||||
input Value
|
||||
attribute cty.Type
|
||||
validateChange change.ValidateChangeFunc
|
||||
validateListChanges []change.ValidateChangeFunc // Lists are special in some cases.
|
||||
}{
|
||||
"primitive_create": {
|
||||
input: Value{
|
||||
@ -529,6 +598,10 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
|
||||
},
|
||||
attribute: cty.String,
|
||||
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": {
|
||||
input: Value{
|
||||
@ -538,6 +611,10 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
|
||||
},
|
||||
attribute: cty.String,
|
||||
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": {
|
||||
input: Value{
|
||||
@ -547,6 +624,10 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
|
||||
},
|
||||
attribute: cty.String,
|
||||
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": {
|
||||
input: Value{
|
||||
@ -575,6 +656,10 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
|
||||
},
|
||||
attribute: cty.String,
|
||||
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": {
|
||||
input: Value{
|
||||
@ -593,6 +678,10 @@ 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{
|
||||
change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
change.ValidateComputed(nil, plans.Create, false),
|
||||
},
|
||||
},
|
||||
"primitive_update_replace": {
|
||||
input: Value{
|
||||
@ -604,6 +693,10 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
|
||||
},
|
||||
attribute: cty.String,
|
||||
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": {
|
||||
input: Value{
|
||||
@ -640,6 +733,24 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
|
||||
}, defaultCollectionsAction, false)
|
||||
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),
|
||||
},
|
||||
"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 {
|
||||
@ -785,10 +998,27 @@ func unmarshalType(t *testing.T, ctyType cty.Type) json.RawMessage {
|
||||
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
|
||||
// a map with a single element. That single element is the input 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) {
|
||||
case nil:
|
||||
if set, ok := unknown.(bool); (set && ok) || explicit {
|
||||
@ -802,20 +1032,22 @@ func wrapValueInMap(input Value) Value {
|
||||
"element": value,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func wrapValue(input Value, step interface{}, wrap func(interface{}, interface{}, bool) interface{}) Value {
|
||||
return Value{
|
||||
Before: tomap(input.Before, nil, input.BeforeExplicit),
|
||||
After: tomap(input.After, input.Unknown, input.AfterExplicit),
|
||||
Unknown: tomap(input.Unknown, nil, false),
|
||||
BeforeSensitive: tomap(input.BeforeSensitive, nil, false),
|
||||
AfterSensitive: tomap(input.AfterSensitive, nil, false),
|
||||
Before: wrap(input.Before, nil, input.BeforeExplicit),
|
||||
After: wrap(input.After, input.Unknown, input.AfterExplicit),
|
||||
Unknown: wrap(input.Unknown, nil, false),
|
||||
BeforeSensitive: wrap(input.BeforeSensitive, nil, false),
|
||||
AfterSensitive: wrap(input.AfterSensitive, nil, false),
|
||||
ReplacePaths: func() []interface{} {
|
||||
var ret []interface{}
|
||||
for _, path := range input.ReplacePaths {
|
||||
old := path.([]interface{})
|
||||
var updated []interface{}
|
||||
updated = append(updated, "element")
|
||||
updated = append(updated, step)
|
||||
updated = append(updated, old...)
|
||||
ret = append(ret, updated)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user