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:
Liam Cervante
2023-01-09 14:33:01 +01:00
committed by GitHub
parent 69cce3597f
commit 05f1764a0d
10 changed files with 926 additions and 8 deletions

View 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()
}

View File

@@ -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)))
}

View File

@@ -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) {

View File

@@ -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, ", "))
}
}
}

View File

@@ -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)
}
}

View File

@@ -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++

View File

@@ -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()

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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