mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-25 01:00:16 -05:00
Add support for blocks in the structured renderer (#32422)
* 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 support for sets in the structured renderer * goimports * Add support for blocks in the structured renderer * goimports * fix ordering of blocks * remove unused test stub
This commit is contained in:
126
internal/command/jsonformat/change/block.go
Normal file
126
internal/command/jsonformat/change/block.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
var (
|
||||
importantAttributes = []string{
|
||||
"id",
|
||||
}
|
||||
)
|
||||
|
||||
func importantAttribute(attr string) bool {
|
||||
for _, attribute := range importantAttributes {
|
||||
if attribute == attr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Block(attributes map[string]Change, blocks map[string][]Change) Renderer {
|
||||
maximumKeyLen := 0
|
||||
for key := range attributes {
|
||||
if len(key) > maximumKeyLen {
|
||||
maximumKeyLen = len(key)
|
||||
}
|
||||
}
|
||||
|
||||
return &blockRenderer{
|
||||
attributes: attributes,
|
||||
blocks: blocks,
|
||||
maximumKeyLen: maximumKeyLen,
|
||||
}
|
||||
}
|
||||
|
||||
type blockRenderer struct {
|
||||
NoWarningsRenderer
|
||||
|
||||
attributes map[string]Change
|
||||
blocks map[string][]Change
|
||||
maximumKeyLen int
|
||||
}
|
||||
|
||||
func (renderer blockRenderer) Render(change Change, indent int, opts RenderOpts) string {
|
||||
unchangedAttributes := 0
|
||||
unchangedBlocks := 0
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(fmt.Sprintf("{%s\n", change.forcesReplacement()))
|
||||
for _, importantKey := range importantAttributes {
|
||||
if attribute, ok := renderer.attributes[importantKey]; ok {
|
||||
if attribute.action == plans.NoOp {
|
||||
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", change.indent(indent+1), attribute.emptySymbol(), renderer.maximumKeyLen, importantKey, attribute.Render(indent+1, opts)))
|
||||
continue
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", change.indent(indent+1), format.DiffActionSymbol(attribute.action), renderer.maximumKeyLen, importantKey, attribute.Render(indent+1, opts)))
|
||||
}
|
||||
}
|
||||
|
||||
var attributeKeys []string
|
||||
for key := range renderer.attributes {
|
||||
attributeKeys = append(attributeKeys, key)
|
||||
}
|
||||
sort.Strings(attributeKeys)
|
||||
|
||||
for _, key := range attributeKeys {
|
||||
if importantAttribute(key) {
|
||||
continue
|
||||
}
|
||||
attribute := renderer.attributes[key]
|
||||
if attribute.action == plans.NoOp && !opts.showUnchangedChildren {
|
||||
unchangedAttributes++
|
||||
continue
|
||||
}
|
||||
|
||||
for _, warning := range attribute.Warnings(indent + 1) {
|
||||
buf.WriteString(fmt.Sprintf("%s%s\n", change.indent(indent+1), warning))
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", change.indent(indent+1), format.DiffActionSymbol(attribute.action), renderer.maximumKeyLen, key, attribute.Render(indent+1, opts)))
|
||||
}
|
||||
|
||||
if unchangedAttributes > 0 {
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s\n", change.indent(indent+1), change.emptySymbol(), change.unchanged("attribute", unchangedAttributes)))
|
||||
}
|
||||
|
||||
var blockKeys []string
|
||||
for key := range renderer.blocks {
|
||||
blockKeys = append(blockKeys, key)
|
||||
}
|
||||
sort.Strings(blockKeys)
|
||||
|
||||
for _, key := range blockKeys {
|
||||
blocks := renderer.blocks[key]
|
||||
|
||||
foundChangedBlock := false
|
||||
for _, block := range blocks {
|
||||
if block.action == plans.NoOp && !opts.showUnchangedChildren {
|
||||
unchangedBlocks++
|
||||
continue
|
||||
}
|
||||
|
||||
if !foundChangedBlock && len(renderer.attributes) > 0 {
|
||||
buf.WriteString("\n")
|
||||
foundChangedBlock = true
|
||||
}
|
||||
|
||||
for _, warning := range block.Warnings(indent + 1) {
|
||||
buf.WriteString(fmt.Sprintf("%s%s\n", change.indent(indent+1), warning))
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s %s\n", change.indent(indent+1), format.DiffActionSymbol(block.action), key, block.Render(indent+1, opts)))
|
||||
}
|
||||
}
|
||||
|
||||
if unchangedBlocks > 0 {
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s\n", change.indent(indent+1), change.emptySymbol(), change.unchanged("block", unchangedBlocks)))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("%s%s }", change.indent(indent), change.emptySymbol()))
|
||||
return buf.String()
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func (renderer listRenderer) Render(change Change, indent int, opts RenderOpts)
|
||||
// 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)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s,\n", change.indent(indent+1), lastElement.emptySymbol(), 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
|
||||
@@ -96,7 +96,7 @@ func (renderer listRenderer) Render(change Change, indent int, opts RenderOpts)
|
||||
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)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s,\n", change.indent(indent+1), element.emptySymbol(), 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)))
|
||||
}
|
||||
|
||||
@@ -1157,6 +1157,320 @@ func TestRenderers(t *testing.T) {
|
||||
]
|
||||
`,
|
||||
},
|
||||
"create_empty_block": {
|
||||
change: Change{
|
||||
renderer: Block(nil, nil),
|
||||
action: plans.Create,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
}`,
|
||||
},
|
||||
"create_populated_block": {
|
||||
change: Change{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(nil, strptr("\"root\"")),
|
||||
action: plans.Create,
|
||||
},
|
||||
"boolean": {
|
||||
renderer: Primitive(nil, strptr("true")),
|
||||
action: plans.Create,
|
||||
},
|
||||
}, map[string][]Change{
|
||||
"nested_block": {
|
||||
{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(nil, strptr("\"one\"")),
|
||||
action: plans.Create,
|
||||
},
|
||||
}, nil),
|
||||
action: plans.Create,
|
||||
},
|
||||
},
|
||||
"nested_block_two": {
|
||||
{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(nil, strptr("\"two\"")),
|
||||
action: plans.Create,
|
||||
},
|
||||
}, nil),
|
||||
action: plans.Create,
|
||||
},
|
||||
},
|
||||
}),
|
||||
action: plans.Create,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
+ boolean = true
|
||||
+ string = "root"
|
||||
|
||||
+ nested_block {
|
||||
+ string = "one"
|
||||
}
|
||||
|
||||
+ nested_block_two {
|
||||
+ string = "two"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
"update_empty_block": {
|
||||
change: Change{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(nil, strptr("\"root\"")),
|
||||
action: plans.Create,
|
||||
},
|
||||
"boolean": {
|
||||
renderer: Primitive(nil, strptr("true")),
|
||||
action: plans.Create,
|
||||
},
|
||||
}, map[string][]Change{
|
||||
"nested_block": {
|
||||
{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(nil, strptr("\"one\"")),
|
||||
action: plans.Create,
|
||||
},
|
||||
}, nil),
|
||||
action: plans.Create,
|
||||
},
|
||||
},
|
||||
"nested_block_two": {
|
||||
{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(nil, strptr("\"two\"")),
|
||||
action: plans.Create,
|
||||
},
|
||||
}, nil),
|
||||
action: plans.Create,
|
||||
},
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
+ boolean = true
|
||||
+ string = "root"
|
||||
|
||||
+ nested_block {
|
||||
+ string = "one"
|
||||
}
|
||||
|
||||
+ nested_block_two {
|
||||
+ string = "two"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
"update_populated_block": {
|
||||
change: Change{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(nil, strptr("\"root\"")),
|
||||
action: plans.Create,
|
||||
},
|
||||
"boolean": {
|
||||
renderer: Primitive(strptr("false"), strptr("true")),
|
||||
action: plans.Update,
|
||||
},
|
||||
}, map[string][]Change{
|
||||
"nested_block": {
|
||||
{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(nil, strptr("\"one\"")),
|
||||
action: plans.NoOp,
|
||||
},
|
||||
}, nil),
|
||||
action: plans.NoOp,
|
||||
},
|
||||
},
|
||||
"nested_block_two": {
|
||||
{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(nil, strptr("\"two\"")),
|
||||
action: plans.Create,
|
||||
},
|
||||
}, nil),
|
||||
action: plans.Create,
|
||||
},
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
~ boolean = false -> true
|
||||
+ string = "root"
|
||||
|
||||
+ nested_block_two {
|
||||
+ string = "two"
|
||||
}
|
||||
# (1 unchanged block hidden)
|
||||
}`,
|
||||
},
|
||||
"clear_populated_block": {
|
||||
change: Change{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(strptr("\"root\""), nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
"boolean": {
|
||||
renderer: Primitive(strptr("true"), nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
}, map[string][]Change{
|
||||
"nested_block": {
|
||||
{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(strptr("\"one\""), nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
}, nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
},
|
||||
"nested_block_two": {
|
||||
{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(strptr("\"two\""), nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
}, nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
- boolean = true -> null
|
||||
- string = "root" -> null
|
||||
|
||||
- nested_block {
|
||||
- string = "one" -> null
|
||||
}
|
||||
|
||||
- nested_block_two {
|
||||
- string = "two" -> null
|
||||
}
|
||||
}`,
|
||||
},
|
||||
"delete_populated_block": {
|
||||
change: Change{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(strptr("\"root\""), nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
"boolean": {
|
||||
renderer: Primitive(strptr("true"), nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
}, map[string][]Change{
|
||||
"nested_block": {
|
||||
{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(strptr("\"one\""), nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
}, nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
},
|
||||
"nested_block_two": {
|
||||
{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(strptr("\"two\""), nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
}, nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
},
|
||||
}),
|
||||
action: plans.Delete,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
- boolean = true -> null
|
||||
- string = "root" -> null
|
||||
|
||||
- nested_block {
|
||||
- string = "one" -> null
|
||||
}
|
||||
|
||||
- nested_block_two {
|
||||
- string = "two" -> null
|
||||
}
|
||||
}`,
|
||||
},
|
||||
"delete_empty_block": {
|
||||
change: Change{
|
||||
renderer: Block(nil, nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
}`,
|
||||
},
|
||||
"block_always_includes_important_attributes": {
|
||||
change: Change{
|
||||
renderer: Block(map[string]Change{
|
||||
"id": {
|
||||
renderer: Primitive(strptr("\"root\""), strptr("\"root\"")),
|
||||
action: plans.NoOp,
|
||||
},
|
||||
"boolean": {
|
||||
renderer: Primitive(strptr("false"), strptr("false")),
|
||||
action: plans.NoOp,
|
||||
},
|
||||
}, map[string][]Change{
|
||||
"nested_block": {
|
||||
{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(strptr("\"one\""), strptr("\"one\"")),
|
||||
action: plans.NoOp,
|
||||
},
|
||||
}, nil),
|
||||
action: plans.NoOp,
|
||||
},
|
||||
},
|
||||
"nested_block_two": {
|
||||
{
|
||||
renderer: Block(map[string]Change{
|
||||
"string": {
|
||||
renderer: Primitive(strptr("\"two\""), strptr("\"two\"")),
|
||||
action: plans.NoOp,
|
||||
},
|
||||
}, nil),
|
||||
action: plans.NoOp,
|
||||
},
|
||||
},
|
||||
}),
|
||||
action: plans.NoOp,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
id = "root"
|
||||
# (1 unchanged attribute hidden)
|
||||
# (2 unchanged blocks hidden)
|
||||
}`,
|
||||
},
|
||||
}
|
||||
for name, tc := range tcs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
||||
@@ -186,7 +186,60 @@ func ValidateSet(elements []ValidateChangeFunc, action plans.Action, replace boo
|
||||
for ix := 0; ix < len(elements); ix++ {
|
||||
elements[ix](t, set.elements[ix])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateBlock(attributes map[string]ValidateChangeFunc, blocks map[string][]ValidateChangeFunc, action plans.Action, replace bool) ValidateChangeFunc {
|
||||
return func(t *testing.T, change Change) {
|
||||
validateChange(t, change, action, replace)
|
||||
|
||||
block, ok := change.renderer.(*blockRenderer)
|
||||
if !ok {
|
||||
t.Fatalf("invalid renderer type: %T", change.renderer)
|
||||
}
|
||||
|
||||
if len(block.attributes) != len(attributes) || len(block.blocks) != len(blocks) {
|
||||
t.Fatalf("expected %d attributes and %d blocks but found %d attributes and %d blocks", len(attributes), len(blocks), len(block.attributes), len(block.blocks))
|
||||
}
|
||||
|
||||
var missingAttributes []string
|
||||
var missingBlocks []string
|
||||
|
||||
for key, expected := range attributes {
|
||||
actual, ok := block.attributes[key]
|
||||
if !ok {
|
||||
missingAttributes = append(missingAttributes, key)
|
||||
}
|
||||
|
||||
if len(missingAttributes) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
expected(t, actual)
|
||||
}
|
||||
|
||||
for key, expected := range blocks {
|
||||
actual, ok := block.blocks[key]
|
||||
if !ok {
|
||||
missingBlocks = append(missingBlocks, key)
|
||||
}
|
||||
|
||||
if len(missingAttributes) > 0 || len(missingBlocks) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(expected) != len(actual) {
|
||||
t.Fatalf("expected %d blocks for %s but found %d", len(expected), key, len(actual))
|
||||
}
|
||||
|
||||
for ix := range expected {
|
||||
expected[ix](t, actual[ix])
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingAttributes) > 0 || len(missingBlocks) > 0 {
|
||||
t.Fatalf("missing the following attributes: %s, and the following blocks: %s", strings.Join(missingAttributes, ", "), strings.Join(missingBlocks, ", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,54 @@ package differ
|
||||
import (
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/change"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
func (v Value) ComputeChangeForBlock(block *jsonprovider.Block) change.Change {
|
||||
panic("not implemented")
|
||||
func (v Value) computeChangeForBlock(block *jsonprovider.Block) change.Change {
|
||||
current := v.getDefaultActionForIteration()
|
||||
|
||||
blockValue := v.asMap()
|
||||
|
||||
attributes := make(map[string]change.Change)
|
||||
for key, attr := range block.Attributes {
|
||||
childValue := blockValue.getChild(key)
|
||||
childChange := childValue.ComputeChange(attr)
|
||||
if childChange.GetAction() == plans.NoOp && childValue.Before == nil && childValue.After == nil {
|
||||
// Don't record nil values at all in blocks.
|
||||
continue
|
||||
}
|
||||
|
||||
attributes[key] = childChange
|
||||
current = compareActions(current, childChange.GetAction())
|
||||
}
|
||||
|
||||
blocks := make(map[string][]change.Change)
|
||||
for key, blockType := range block.BlockTypes {
|
||||
childValue := blockValue.getChild(key)
|
||||
childChanges, next := childValue.computeChangesForBlockType(blockType)
|
||||
if next == plans.NoOp && childValue.Before == nil && childValue.After == nil {
|
||||
// Don't record nil values at all in blocks.
|
||||
continue
|
||||
}
|
||||
blocks[key] = childChanges
|
||||
current = compareActions(current, next)
|
||||
}
|
||||
|
||||
return change.New(change.Block(attributes, blocks), current, v.replacePath())
|
||||
}
|
||||
|
||||
func (v Value) computeChangesForBlockType(blockType *jsonprovider.BlockType) ([]change.Change, plans.Action) {
|
||||
switch blockType.NestingMode {
|
||||
case "set":
|
||||
return v.computeBlockChangesAsSet(blockType.Block)
|
||||
case "list":
|
||||
return v.computeBlockChangesAsList(blockType.Block)
|
||||
case "map":
|
||||
return v.computeBlockChangesAsMap(blockType.Block)
|
||||
case "single", "group":
|
||||
ch := v.ComputeChange(blockType.Block)
|
||||
return []change.Change{ch}, ch.GetAction()
|
||||
default:
|
||||
panic("unrecognized nesting mode: " + blockType.NestingMode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ import (
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/change"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
func (v Value) computeAttributeChangeAsList(elementType cty.Type) change.Change {
|
||||
var elements []change.Change
|
||||
current := v.getDefaultActionForIteration()
|
||||
v.processList(elementType, func(value Value) {
|
||||
v.processList(elementType.IsObjectType(), func(value Value) {
|
||||
element := value.ComputeChange(elementType)
|
||||
elements = append(elements, element)
|
||||
current = compareActions(current, element.GetAction())
|
||||
@@ -31,6 +32,17 @@ func (v Value) computeAttributeChangeAsNestedList(attributes map[string]*jsonpro
|
||||
return change.New(change.NestedList(elements), current, v.replacePath())
|
||||
}
|
||||
|
||||
func (v Value) computeBlockChangesAsList(block *jsonprovider.Block) ([]change.Change, plans.Action) {
|
||||
var elements []change.Change
|
||||
current := v.getDefaultActionForIteration()
|
||||
v.processNestedList(func(value Value) {
|
||||
element := value.ComputeChange(block)
|
||||
elements = append(elements, element)
|
||||
current = compareActions(current, element.GetAction())
|
||||
})
|
||||
return elements, current
|
||||
}
|
||||
|
||||
func (v Value) processNestedList(process func(value Value)) {
|
||||
sliceValue := v.asSlice()
|
||||
for ix := 0; ix < len(sliceValue.Before) || ix < len(sliceValue.After); ix++ {
|
||||
@@ -38,7 +50,7 @@ func (v Value) processNestedList(process func(value Value)) {
|
||||
}
|
||||
}
|
||||
|
||||
func (v Value) processList(elementType cty.Type, process func(value Value)) {
|
||||
func (v Value) processList(isObjType bool, process func(value Value)) {
|
||||
sliceValue := v.asSlice()
|
||||
|
||||
lcs := lcs(sliceValue.Before, sliceValue.After)
|
||||
@@ -48,7 +60,7 @@ func (v Value) processList(elementType cty.Type, process func(value Value)) {
|
||||
// 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]))
|
||||
isObjectDiff := isObjType && afterIx < len(sliceValue.After) && (lcsIx >= len(lcs) || !reflect.DeepEqual(sliceValue.After[afterIx], lcs[lcsIx]))
|
||||
if isObjectDiff {
|
||||
process(sliceValue.getChild(beforeIx, afterIx, false))
|
||||
beforeIx++
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/change"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
func (v Value) computeAttributeChangeAsMap(elementType cty.Type) change.Change {
|
||||
@@ -29,6 +30,17 @@ func (v Value) computeAttributeChangeAsNestedMap(attributes map[string]*jsonprov
|
||||
return change.New(change.Map(elements), current, v.replacePath())
|
||||
}
|
||||
|
||||
func (v Value) computeBlockChangesAsMap(block *jsonprovider.Block) ([]change.Change, plans.Action) {
|
||||
current := v.getDefaultActionForIteration()
|
||||
var elements []change.Change
|
||||
v.processMap(func(key string, value Value) {
|
||||
element := value.ComputeChange(block)
|
||||
elements = append(elements, element)
|
||||
current = compareActions(current, element.GetAction())
|
||||
})
|
||||
return elements, current
|
||||
}
|
||||
|
||||
func (v Value) processMap(process func(key string, value Value)) {
|
||||
mapValue := v.asMap()
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/change"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
func (v Value) computeAttributeChangeAsSet(elementType cty.Type) change.Change {
|
||||
@@ -31,6 +32,17 @@ func (v Value) computeAttributeChangeAsNestedSet(attributes map[string]*jsonprov
|
||||
return change.New(change.Set(elements), current, v.replacePath())
|
||||
}
|
||||
|
||||
func (v Value) computeBlockChangesAsSet(block *jsonprovider.Block) ([]change.Change, plans.Action) {
|
||||
var elements []change.Change
|
||||
current := v.getDefaultActionForIteration()
|
||||
v.processSet(true, func(value Value) {
|
||||
element := value.ComputeChange(block)
|
||||
elements = append(elements, element)
|
||||
current = compareActions(current, element.GetAction())
|
||||
})
|
||||
return elements, current
|
||||
}
|
||||
|
||||
func (v Value) processSet(propagateReplace bool, process func(value Value)) {
|
||||
sliceValue := v.asSlice()
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
// jsonprovider).
|
||||
//
|
||||
// A Value can be converted into a change.Change, ready for rendering, with the
|
||||
// computeChangeForAttribute, ComputeChangeForOutput, and ComputeChangeForBlock
|
||||
// computeChangeForAttribute, ComputeChangeForOutput, and computeChangeForBlock
|
||||
// functions.
|
||||
//
|
||||
// The Before and After fields are actually go-cty values, but we cannot convert
|
||||
@@ -121,6 +121,8 @@ func (v Value) ComputeChange(changeType interface{}) change.Change {
|
||||
return v.computeChangeForType(concrete)
|
||||
case map[string]*jsonprovider.Attribute:
|
||||
return v.computeAttributeChangeAsNestedObject(concrete)
|
||||
case *jsonprovider.Block:
|
||||
return v.computeChangeForBlock(concrete)
|
||||
default:
|
||||
panic(fmt.Sprintf("unrecognized change type: %T", changeType))
|
||||
}
|
||||
@@ -185,6 +187,10 @@ func (v Value) getDefaultActionForIteration() plans.Action {
|
||||
// to convert a NoOp default action into an Update based on the actions of a
|
||||
// values children.
|
||||
func compareActions(current, next plans.Action) plans.Action {
|
||||
if next == plans.NoOp {
|
||||
return current
|
||||
}
|
||||
|
||||
if current != next {
|
||||
return plans.Update
|
||||
}
|
||||
|
||||
@@ -871,6 +871,343 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValue_BlockAttributesAndNestedBlocks(t *testing.T) {
|
||||
// This function tests manipulating simple attributes and blocks within
|
||||
// blocks. It automatically tests these operations within the contexts of
|
||||
// different block types.
|
||||
|
||||
tcs := map[string]struct {
|
||||
before interface{}
|
||||
after interface{}
|
||||
block *jsonprovider.Block
|
||||
validate change.ValidateChangeFunc
|
||||
validateSet []change.ValidateChangeFunc
|
||||
}{
|
||||
"create_attribute": {
|
||||
before: map[string]interface{}{},
|
||||
after: map[string]interface{}{
|
||||
"attribute_one": "new",
|
||||
},
|
||||
block: &jsonprovider.Block{
|
||||
Attributes: map[string]*jsonprovider.Attribute{
|
||||
"attribute_one": {
|
||||
AttributeType: unmarshalType(t, cty.String),
|
||||
},
|
||||
},
|
||||
},
|
||||
validate: change.ValidateBlock(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
|
||||
}, nil, plans.Update, false),
|
||||
validateSet: []change.ValidateChangeFunc{
|
||||
change.ValidateBlock(nil, nil, plans.Delete, false),
|
||||
change.ValidateBlock(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
|
||||
}, nil, plans.Create, false),
|
||||
},
|
||||
},
|
||||
"update_attribute": {
|
||||
before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
},
|
||||
after: map[string]interface{}{
|
||||
"attribute_one": "new",
|
||||
},
|
||||
block: &jsonprovider.Block{
|
||||
Attributes: map[string]*jsonprovider.Attribute{
|
||||
"attribute_one": {
|
||||
AttributeType: unmarshalType(t, cty.String),
|
||||
},
|
||||
},
|
||||
},
|
||||
validate: change.ValidateBlock(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, false),
|
||||
}, nil, plans.Update, false),
|
||||
validateSet: []change.ValidateChangeFunc{
|
||||
change.ValidateBlock(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
}, nil, plans.Delete, false),
|
||||
change.ValidateBlock(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
|
||||
}, nil, plans.Create, false),
|
||||
},
|
||||
},
|
||||
"delete_attribute": {
|
||||
before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
},
|
||||
after: map[string]interface{}{},
|
||||
block: &jsonprovider.Block{
|
||||
Attributes: map[string]*jsonprovider.Attribute{
|
||||
"attribute_one": {
|
||||
AttributeType: unmarshalType(t, cty.String),
|
||||
},
|
||||
},
|
||||
},
|
||||
validate: change.ValidateBlock(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
}, nil, plans.Update, false),
|
||||
validateSet: []change.ValidateChangeFunc{
|
||||
change.ValidateBlock(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
}, nil, plans.Delete, false),
|
||||
change.ValidateBlock(nil, nil, plans.Create, false),
|
||||
},
|
||||
},
|
||||
"create_block": {
|
||||
before: map[string]interface{}{},
|
||||
after: map[string]interface{}{
|
||||
"block_one": map[string]interface{}{
|
||||
"attribute_one": "new",
|
||||
},
|
||||
},
|
||||
block: &jsonprovider.Block{
|
||||
BlockTypes: map[string]*jsonprovider.BlockType{
|
||||
"block_one": {
|
||||
Block: &jsonprovider.Block{
|
||||
Attributes: map[string]*jsonprovider.Attribute{
|
||||
"attribute_one": {
|
||||
AttributeType: unmarshalType(t, cty.String),
|
||||
},
|
||||
},
|
||||
},
|
||||
NestingMode: "single",
|
||||
},
|
||||
},
|
||||
},
|
||||
validate: change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{
|
||||
"block_one": {
|
||||
change.ValidateBlock(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
|
||||
}, nil, plans.Create, false),
|
||||
},
|
||||
}, plans.Update, false),
|
||||
validateSet: []change.ValidateChangeFunc{
|
||||
change.ValidateBlock(nil, nil, plans.Delete, false),
|
||||
change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{
|
||||
"block_one": {
|
||||
change.ValidateBlock(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
|
||||
}, nil, plans.Create, false),
|
||||
},
|
||||
}, plans.Create, false),
|
||||
},
|
||||
},
|
||||
"update_block": {
|
||||
before: map[string]interface{}{
|
||||
"block_one": map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
},
|
||||
},
|
||||
after: map[string]interface{}{
|
||||
"block_one": map[string]interface{}{
|
||||
"attribute_one": "new",
|
||||
},
|
||||
},
|
||||
block: &jsonprovider.Block{
|
||||
BlockTypes: map[string]*jsonprovider.BlockType{
|
||||
"block_one": {
|
||||
Block: &jsonprovider.Block{
|
||||
Attributes: map[string]*jsonprovider.Attribute{
|
||||
"attribute_one": {
|
||||
AttributeType: unmarshalType(t, cty.String),
|
||||
},
|
||||
},
|
||||
},
|
||||
NestingMode: "single",
|
||||
},
|
||||
},
|
||||
},
|
||||
validate: change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{
|
||||
"block_one": {
|
||||
change.ValidateBlock(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, false),
|
||||
}, nil, plans.Update, false),
|
||||
},
|
||||
}, plans.Update, false),
|
||||
validateSet: []change.ValidateChangeFunc{
|
||||
change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{
|
||||
"block_one": {
|
||||
change.ValidateBlock(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
}, nil, plans.Delete, false),
|
||||
},
|
||||
}, plans.Delete, false),
|
||||
change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{
|
||||
"block_one": {
|
||||
change.ValidateBlock(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
|
||||
}, nil, plans.Create, false),
|
||||
},
|
||||
}, plans.Create, false),
|
||||
},
|
||||
},
|
||||
"delete_block": {
|
||||
before: map[string]interface{}{
|
||||
"block_one": map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
},
|
||||
},
|
||||
after: map[string]interface{}{},
|
||||
block: &jsonprovider.Block{
|
||||
BlockTypes: map[string]*jsonprovider.BlockType{
|
||||
"block_one": {
|
||||
Block: &jsonprovider.Block{
|
||||
Attributes: map[string]*jsonprovider.Attribute{
|
||||
"attribute_one": {
|
||||
AttributeType: unmarshalType(t, cty.String),
|
||||
},
|
||||
},
|
||||
},
|
||||
NestingMode: "single",
|
||||
},
|
||||
},
|
||||
},
|
||||
validate: change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{
|
||||
"block_one": {
|
||||
change.ValidateBlock(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
}, nil, plans.Delete, false),
|
||||
},
|
||||
}, plans.Update, false),
|
||||
validateSet: []change.ValidateChangeFunc{
|
||||
change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{
|
||||
"block_one": {
|
||||
change.ValidateBlock(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
}, nil, plans.Delete, false),
|
||||
},
|
||||
}, plans.Delete, false),
|
||||
change.ValidateBlock(nil, nil, plans.Create, false),
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tmp := range tcs {
|
||||
tc := tmp
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Run("single", func(t *testing.T) {
|
||||
input := Value{
|
||||
Before: map[string]interface{}{
|
||||
"block_type": tc.before,
|
||||
},
|
||||
After: map[string]interface{}{
|
||||
"block_type": tc.after,
|
||||
},
|
||||
}
|
||||
|
||||
block := &jsonprovider.Block{
|
||||
BlockTypes: map[string]*jsonprovider.BlockType{
|
||||
"block_type": {
|
||||
Block: tc.block,
|
||||
NestingMode: "single",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
validate := change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{
|
||||
"block_type": {
|
||||
tc.validate,
|
||||
},
|
||||
}, plans.Update, false)
|
||||
validate(t, input.ComputeChange(block))
|
||||
})
|
||||
t.Run("map", func(t *testing.T) {
|
||||
input := Value{
|
||||
Before: map[string]interface{}{
|
||||
"block_type": map[string]interface{}{
|
||||
"one": tc.before,
|
||||
},
|
||||
},
|
||||
After: map[string]interface{}{
|
||||
"block_type": map[string]interface{}{
|
||||
"one": tc.after,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
block := &jsonprovider.Block{
|
||||
BlockTypes: map[string]*jsonprovider.BlockType{
|
||||
"block_type": {
|
||||
Block: tc.block,
|
||||
NestingMode: "map",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
validate := change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{
|
||||
"block_type": {
|
||||
tc.validate,
|
||||
},
|
||||
}, plans.Update, false)
|
||||
validate(t, input.ComputeChange(block))
|
||||
})
|
||||
t.Run("list", func(t *testing.T) {
|
||||
input := Value{
|
||||
Before: map[string]interface{}{
|
||||
"block_type": []interface{}{
|
||||
tc.before,
|
||||
},
|
||||
},
|
||||
After: map[string]interface{}{
|
||||
"block_type": []interface{}{
|
||||
tc.after,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
block := &jsonprovider.Block{
|
||||
BlockTypes: map[string]*jsonprovider.BlockType{
|
||||
"block_type": {
|
||||
Block: tc.block,
|
||||
NestingMode: "list",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
validate := change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{
|
||||
"block_type": {
|
||||
tc.validate,
|
||||
},
|
||||
}, plans.Update, false)
|
||||
validate(t, input.ComputeChange(block))
|
||||
})
|
||||
t.Run("set", func(t *testing.T) {
|
||||
input := Value{
|
||||
Before: map[string]interface{}{
|
||||
"block_type": []interface{}{
|
||||
tc.before,
|
||||
},
|
||||
},
|
||||
After: map[string]interface{}{
|
||||
"block_type": []interface{}{
|
||||
tc.after,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
block := &jsonprovider.Block{
|
||||
BlockTypes: map[string]*jsonprovider.BlockType{
|
||||
"block_type": {
|
||||
Block: tc.block,
|
||||
NestingMode: "set",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
validate := change.ValidateBlock(nil, map[string][]change.ValidateChangeFunc{
|
||||
"block_type": func() []change.ValidateChangeFunc {
|
||||
if tc.validateSet != nil {
|
||||
return tc.validateSet
|
||||
}
|
||||
return []change.ValidateChangeFunc{tc.validate}
|
||||
}(),
|
||||
}, plans.Update, false)
|
||||
validate(t, input.ComputeChange(block))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValue_PrimitiveAttributes(t *testing.T) {
|
||||
// This function tests manipulating primitives: creating, deleting and
|
||||
// updating. It also automatically tests these operations within the
|
||||
|
||||
Reference in New Issue
Block a user