mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Add support for maps in the structured renderer (#32397)
* 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
This commit is contained in:
parent
b097d8873d
commit
8975eebf84
83
internal/command/jsonformat/change/map.go
Normal file
83
internal/command/jsonformat/change/map.go
Normal file
@ -0,0 +1,83 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
func Map(elements map[string]Change) Renderer {
|
||||
maximumKeyLen := 0
|
||||
for key := range elements {
|
||||
if maximumKeyLen < len(key) {
|
||||
maximumKeyLen = len(key)
|
||||
}
|
||||
}
|
||||
|
||||
return &mapRenderer{
|
||||
elements: elements,
|
||||
maximumKeyLen: maximumKeyLen,
|
||||
}
|
||||
}
|
||||
|
||||
type mapRenderer struct {
|
||||
NoWarningsRenderer
|
||||
|
||||
elements map[string]Change
|
||||
maximumKeyLen int
|
||||
}
|
||||
|
||||
func (renderer mapRenderer) Render(change Change, indent int, opts RenderOpts) string {
|
||||
if len(renderer.elements) == 0 {
|
||||
return fmt.Sprintf("{}%s%s", change.nullSuffix(opts.overrideNullSuffix), change.forcesReplacement())
|
||||
}
|
||||
|
||||
unchangedElements := 0
|
||||
|
||||
// Sort the map elements by key, so we have a deterministic ordering in
|
||||
// the output.
|
||||
var keys []string
|
||||
for key := range renderer.elements {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
elementOpts := opts.Clone()
|
||||
if change.action == plans.Delete {
|
||||
elementOpts.overrideNullSuffix = true
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(fmt.Sprintf("{%s\n", change.forcesReplacement()))
|
||||
for _, key := range keys {
|
||||
element := renderer.elements[key]
|
||||
|
||||
if element.action == plans.NoOp && !opts.showUnchangedChildren {
|
||||
// Don't render NoOp operations when we are compact display.
|
||||
unchangedElements++
|
||||
continue
|
||||
}
|
||||
|
||||
for _, warning := range element.Warnings(indent + 1) {
|
||||
buf.WriteString(fmt.Sprintf("%s%s\n", change.indent(indent+1), warning))
|
||||
}
|
||||
|
||||
// Only show commas between elements for objects.
|
||||
comma := ""
|
||||
if _, ok := element.renderer.(objectRenderer); ok {
|
||||
comma = ","
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("%s%s \"%s\"%-*s = %s%s\n", change.indent(indent+1), format.DiffActionSymbol(element.action), key, renderer.maximumKeyLen-len(key), "", element.Render(indent+1, elementOpts), comma))
|
||||
}
|
||||
|
||||
if unchangedElements > 0 {
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s\n", change.indent(indent+1), change.emptySymbol(), change.unchanged("element", unchangedElements)))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("%s%s }%s", change.indent(indent), change.emptySymbol(), change.nullSuffix(opts.overrideNullSuffix)))
|
||||
return buf.String()
|
||||
}
|
@ -349,6 +349,226 @@ func TestRenderers(t *testing.T) {
|
||||
{
|
||||
~ attribute_one = 1 -> (known after apply)
|
||||
}
|
||||
`,
|
||||
},
|
||||
"map_create_empty": {
|
||||
change: Change{
|
||||
renderer: Map(map[string]Change{}),
|
||||
action: plans.Create,
|
||||
},
|
||||
expected: "{}",
|
||||
},
|
||||
"map_create": {
|
||||
change: Change{
|
||||
renderer: Map(map[string]Change{
|
||||
"element_one": {
|
||||
renderer: Primitive(nil, strptr("new")),
|
||||
action: plans.Create,
|
||||
},
|
||||
}),
|
||||
action: plans.Create,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
+ "element_one" = new
|
||||
}
|
||||
`,
|
||||
},
|
||||
"map_delete_empty": {
|
||||
change: Change{
|
||||
renderer: Map(map[string]Change{}),
|
||||
action: plans.Delete,
|
||||
},
|
||||
expected: "{} -> null",
|
||||
},
|
||||
"map_delete": {
|
||||
change: Change{
|
||||
renderer: Map(map[string]Change{
|
||||
"element_one": {
|
||||
renderer: Primitive(strptr("old"), nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
}),
|
||||
action: plans.Delete,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
- "element_one" = old
|
||||
} -> null
|
||||
`,
|
||||
},
|
||||
"map_create_element": {
|
||||
change: Change{
|
||||
renderer: Map(map[string]Change{
|
||||
"element_one": {
|
||||
renderer: Primitive(nil, strptr("new")),
|
||||
action: plans.Create,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
+ "element_one" = new
|
||||
}
|
||||
`,
|
||||
},
|
||||
"map_update_element": {
|
||||
change: Change{
|
||||
renderer: Map(map[string]Change{
|
||||
"element_one": {
|
||||
renderer: Primitive(strptr("old"), strptr("new")),
|
||||
action: plans.Update,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
~ "element_one" = old -> new
|
||||
}
|
||||
`,
|
||||
},
|
||||
"map_delete_element": {
|
||||
change: Change{
|
||||
renderer: Map(map[string]Change{
|
||||
"element_one": {
|
||||
renderer: Primitive(strptr("old"), nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
- "element_one" = old -> null
|
||||
}
|
||||
`,
|
||||
},
|
||||
"map_update_forces_replacement": {
|
||||
change: Change{
|
||||
renderer: Map(map[string]Change{
|
||||
"element_one": {
|
||||
renderer: Primitive(strptr("old"), strptr("new")),
|
||||
action: plans.Update,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
replace: true,
|
||||
},
|
||||
expected: `
|
||||
{ # forces replacement
|
||||
~ "element_one" = old -> new
|
||||
}
|
||||
`,
|
||||
},
|
||||
"map_ignore_unchanged_elements": {
|
||||
change: Change{
|
||||
renderer: Map(map[string]Change{
|
||||
"element_one": {
|
||||
renderer: Primitive(nil, strptr("new")),
|
||||
action: plans.Create,
|
||||
},
|
||||
"element_two": {
|
||||
renderer: Primitive(strptr("old"), strptr("old")),
|
||||
action: plans.NoOp,
|
||||
},
|
||||
"element_three": {
|
||||
renderer: Primitive(strptr("old"), strptr("new")),
|
||||
action: plans.Update,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
+ "element_one" = new
|
||||
~ "element_three" = old -> new
|
||||
# (1 unchanged element hidden)
|
||||
}
|
||||
`,
|
||||
},
|
||||
"map_create_sensitive_element": {
|
||||
change: Change{
|
||||
renderer: Map(map[string]Change{
|
||||
"element_one": {
|
||||
renderer: Sensitive(nil, 1, false, true),
|
||||
action: plans.Create,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
+ "element_one" = (sensitive)
|
||||
}
|
||||
`,
|
||||
},
|
||||
"map_update_sensitive_element": {
|
||||
change: Change{
|
||||
renderer: Map(map[string]Change{
|
||||
"element_one": {
|
||||
renderer: Sensitive(0, 1, true, true),
|
||||
action: plans.Update,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
~ "element_one" = (sensitive)
|
||||
}
|
||||
`,
|
||||
},
|
||||
"map_delete_sensitive_element": {
|
||||
change: Change{
|
||||
renderer: Map(map[string]Change{
|
||||
"element_one": {
|
||||
renderer: Sensitive(0, nil, true, false),
|
||||
action: plans.Delete,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
- "element_one" = (sensitive) -> null
|
||||
}
|
||||
`,
|
||||
},
|
||||
"map_create_computed_element": {
|
||||
change: Change{
|
||||
renderer: Map(map[string]Change{
|
||||
"element_one": {
|
||||
renderer: Computed(Change{}),
|
||||
action: plans.Create,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
+ "element_one" = (known after apply)
|
||||
}
|
||||
`,
|
||||
},
|
||||
"map_update_computed_element": {
|
||||
change: Change{
|
||||
renderer: Map(map[string]Change{
|
||||
"element_one": {
|
||||
renderer: Computed(Change{
|
||||
renderer: Primitive(strptr("1"), nil),
|
||||
action: plans.Delete,
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
~ "element_one" = 1 -> (known after apply)
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ func (renderer sensitiveRenderer) Render(change Change, indent int, opts RenderO
|
||||
func (renderer sensitiveRenderer) Warnings(change Change, indent int) []string {
|
||||
if (renderer.beforeSensitive == renderer.afterSensitive) || renderer.before == nil || renderer.after == nil {
|
||||
// Only display warnings for sensitive values if they are changing from
|
||||
// being sensitive or to being sensitive or if they are not being
|
||||
// being sensitive or to being sensitive and if they are not being
|
||||
// destroyed or created.
|
||||
return []string{}
|
||||
}
|
||||
|
@ -93,6 +93,39 @@ func validateObject(t *testing.T, object *objectRenderer, attributes map[string]
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateMap(elements map[string]ValidateChangeFunc, action plans.Action, replace bool) ValidateChangeFunc {
|
||||
return func(t *testing.T, change Change) {
|
||||
validateChange(t, change, action, replace)
|
||||
|
||||
m, ok := change.renderer.(*mapRenderer)
|
||||
if !ok {
|
||||
t.Fatalf("invalid renderer type: %T", change.renderer)
|
||||
}
|
||||
|
||||
if len(m.elements) != len(elements) {
|
||||
t.Fatalf("expected %d elements but found %d elements", len(elements), len(m.elements))
|
||||
}
|
||||
|
||||
var missing []string
|
||||
for key, expected := range elements {
|
||||
actual, ok := m.elements[key]
|
||||
if !ok {
|
||||
missing = append(missing, key)
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
expected(t, actual)
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
t.Fatalf("missing the following elements: %s", strings.Join(missing, ", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -19,6 +19,8 @@ func (v Value) computeChangeForNestedAttribute(attribute *jsonprovider.NestedTyp
|
||||
switch attribute.NestingMode {
|
||||
case "single", "group":
|
||||
return v.computeAttributeChangeAsNestedObject(attribute.Attributes)
|
||||
case "map":
|
||||
return v.computeAttributeChangeAsNestedMap(attribute.Attributes)
|
||||
default:
|
||||
panic("unrecognized nesting mode: " + attribute.NestingMode)
|
||||
}
|
||||
@ -30,6 +32,8 @@ func (v Value) computeChangeForType(ctyType cty.Type) change.Change {
|
||||
return v.computeAttributeChangeAsPrimitive(ctyType)
|
||||
case ctyType.IsObjectType():
|
||||
return v.computeAttributeChangeAsObject(ctyType.AttributeTypes())
|
||||
case ctyType.IsMapType():
|
||||
return v.computeAttributeChangeAsMap(ctyType.ElementType())
|
||||
default:
|
||||
panic("not implemented")
|
||||
}
|
||||
|
45
internal/command/jsonformat/differ/map.go
Normal file
45
internal/command/jsonformat/differ/map.go
Normal file
@ -0,0 +1,45 @@
|
||||
package differ
|
||||
|
||||
import (
|
||||
"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 {
|
||||
current := v.getDefaultActionForIteration()
|
||||
elements := make(map[string]change.Change)
|
||||
v.processMap(func(key string, value Value) {
|
||||
element := value.ComputeChange(elementType)
|
||||
elements[key] = element
|
||||
current = compareActions(current, element.GetAction())
|
||||
})
|
||||
return change.New(change.Map(elements), current, v.replacePath())
|
||||
}
|
||||
|
||||
func (v Value) computeAttributeChangeAsNestedMap(attributes map[string]*jsonprovider.Attribute) change.Change {
|
||||
current := v.getDefaultActionForIteration()
|
||||
elements := make(map[string]change.Change)
|
||||
v.processMap(func(key string, value Value) {
|
||||
element := value.ComputeChange(attributes)
|
||||
elements[key] = element
|
||||
current = compareActions(current, element.GetAction())
|
||||
})
|
||||
return change.New(change.Map(elements), current, v.replacePath())
|
||||
}
|
||||
|
||||
func (v Value) processMap(process func(key string, value Value)) {
|
||||
mapValue := v.asMap()
|
||||
|
||||
handled := make(map[string]bool)
|
||||
for key := range mapValue.Before {
|
||||
handled[key] = true
|
||||
process(key, mapValue.getChild(key))
|
||||
}
|
||||
for key := range mapValue.After {
|
||||
if _, ok := handled[key]; ok {
|
||||
continue
|
||||
}
|
||||
process(key, mapValue.getChild(key))
|
||||
}
|
||||
}
|
@ -98,7 +98,7 @@ func ValueFromJsonChange(change jsonplan.Change) Value {
|
||||
}
|
||||
}
|
||||
|
||||
// ComputeChange is a generic function that lets callers no worry about what
|
||||
// ComputeChange is a generic function that lets callers not worry about what
|
||||
// type of change they are processing. In general, this is the function external
|
||||
// users should call as it has some generic preprocessing applicable to all
|
||||
// types.
|
||||
@ -119,6 +119,8 @@ func (v Value) ComputeChange(changeType interface{}) change.Change {
|
||||
return v.computeChangeForAttribute(concrete)
|
||||
case cty.Type:
|
||||
return v.computeChangeForType(concrete)
|
||||
case map[string]*jsonprovider.Attribute:
|
||||
return v.computeAttributeChangeAsNestedObject(concrete)
|
||||
default:
|
||||
panic(fmt.Sprintf("unrecognized change type: %T", changeType))
|
||||
}
|
||||
|
@ -14,8 +14,10 @@ import (
|
||||
)
|
||||
|
||||
func TestValue_ObjectAttributes(t *testing.T) {
|
||||
// We break these tests out into their own function, so we can automatically
|
||||
// test both objects and nested objects together.
|
||||
// This function holds a range of test cases creating, deleting and editing
|
||||
// objects. It is built in such a way that it can automatically test these
|
||||
// operations on objects both directly and nested, as well as within all
|
||||
// types of collections.
|
||||
|
||||
tcs := map[string]struct {
|
||||
input Value
|
||||
@ -27,7 +29,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateReplace bool
|
||||
validateAction plans.Action
|
||||
}{
|
||||
"object_create": {
|
||||
"create": {
|
||||
input: Value{
|
||||
Before: nil,
|
||||
After: map[string]interface{}{
|
||||
@ -43,7 +45,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateAction: plans.Create,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_delete": {
|
||||
"delete": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
@ -59,7 +61,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateAction: plans.Delete,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_create_sensitive": {
|
||||
"create_sensitive": {
|
||||
input: Value{
|
||||
Before: nil,
|
||||
After: map[string]interface{}{
|
||||
@ -74,7 +76,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
"attribute_one": "new",
|
||||
}, false, true, plans.Create, false),
|
||||
},
|
||||
"object_delete_sensitive": {
|
||||
"delete_sensitive": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
@ -89,7 +91,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
"attribute_one": "old",
|
||||
}, nil, true, false, plans.Delete, false),
|
||||
},
|
||||
"object_create_unknown": {
|
||||
"create_unknown": {
|
||||
input: Value{
|
||||
Before: nil,
|
||||
After: nil,
|
||||
@ -100,7 +102,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
},
|
||||
validateSingleChange: change.ValidateComputed(nil, plans.Create, false),
|
||||
},
|
||||
"object_update_unknown": {
|
||||
"update_unknown": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
@ -118,7 +120,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
}, plans.Delete, false), plans.Update, false),
|
||||
},
|
||||
"object_create_attribute": {
|
||||
"create_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{},
|
||||
After: map[string]interface{}{
|
||||
@ -134,7 +136,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_create_attribute_from_explicit_null": {
|
||||
"create_attribute_from_explicit_null": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": nil,
|
||||
@ -152,7 +154,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_delete_attribute": {
|
||||
"delete_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
@ -168,7 +170,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_delete_attribute_to_explicit_null": {
|
||||
"delete_attribute_to_explicit_null": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
@ -186,7 +188,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_update_attribute": {
|
||||
"update_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
@ -204,7 +206,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_create_sensitive_attribute": {
|
||||
"create_sensitive_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{},
|
||||
After: map[string]interface{}{
|
||||
@ -223,7 +225,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_delete_sensitive_attribute": {
|
||||
"delete_sensitive_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
@ -242,7 +244,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_update_sensitive_attribute": {
|
||||
"update_sensitive_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
@ -266,7 +268,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_create_computed_attribute": {
|
||||
"create_computed_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{},
|
||||
After: map[string]interface{}{},
|
||||
@ -283,7 +285,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_update_computed_attribute": {
|
||||
"update_computed_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
@ -305,7 +307,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_ignores_unset_fields": {
|
||||
"ignores_unset_fields": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{},
|
||||
After: map[string]interface{}{},
|
||||
@ -317,7 +319,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateAction: plans.NoOp,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_update_replace_self": {
|
||||
"update_replace_self": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
@ -338,7 +340,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
validateAction: plans.Update,
|
||||
validateReplace: true,
|
||||
},
|
||||
"object_update_replace_attribute": {
|
||||
"update_replace_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
@ -361,81 +363,163 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tcs {
|
||||
for name, tmp := range tcs {
|
||||
tc := tmp
|
||||
|
||||
collectionDefaultAction := plans.Update
|
||||
if name == "ignores_unset_fields" {
|
||||
// Special case for this test, as it is the only one that doesn't
|
||||
// have the collection types return an update.
|
||||
collectionDefaultAction = plans.NoOp
|
||||
}
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Run("object", func(t *testing.T) {
|
||||
attribute := &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.Object(tc.attributes)),
|
||||
}
|
||||
|
||||
attribute := &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.Object(tc.attributes)),
|
||||
}
|
||||
if tc.validateObject != nil {
|
||||
tc.validateObject(t, tc.input.ComputeChange(attribute))
|
||||
return
|
||||
}
|
||||
|
||||
if tc.validateObject != nil {
|
||||
tc.validateObject(t, tc.input.ComputeChange(attribute))
|
||||
return
|
||||
}
|
||||
if tc.validateSingleChange != nil {
|
||||
tc.validateSingleChange(t, tc.input.ComputeChange(attribute))
|
||||
return
|
||||
}
|
||||
|
||||
if tc.validateSingleChange != nil {
|
||||
tc.validateSingleChange(t, tc.input.ComputeChange(attribute))
|
||||
return
|
||||
}
|
||||
validate := change.ValidateObject(tc.validateChanges, tc.validateAction, tc.validateReplace)
|
||||
validate(t, tc.input.ComputeChange(attribute))
|
||||
})
|
||||
|
||||
t.Run("map", func(t *testing.T) {
|
||||
attribute := &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.Map(cty.Object(tc.attributes))),
|
||||
}
|
||||
|
||||
input := wrapValueInMap(tc.input)
|
||||
|
||||
if tc.validateObject != nil {
|
||||
validate := change.ValidateMap(map[string]change.ValidateChangeFunc{
|
||||
"element": tc.validateObject,
|
||||
}, collectionDefaultAction, false)
|
||||
validate(t, input.ComputeChange(attribute))
|
||||
return
|
||||
}
|
||||
|
||||
if tc.validateSingleChange != nil {
|
||||
validate := change.ValidateMap(map[string]change.ValidateChangeFunc{
|
||||
"element": tc.validateSingleChange,
|
||||
}, collectionDefaultAction, false)
|
||||
validate(t, input.ComputeChange(attribute))
|
||||
return
|
||||
}
|
||||
|
||||
validate := change.ValidateMap(map[string]change.ValidateChangeFunc{
|
||||
"element": change.ValidateObject(tc.validateChanges, tc.validateAction, tc.validateReplace),
|
||||
}, collectionDefaultAction, false)
|
||||
validate(t, input.ComputeChange(attribute))
|
||||
})
|
||||
|
||||
validate := change.ValidateObject(tc.validateChanges, tc.validateAction, tc.validateReplace)
|
||||
validate(t, tc.input.ComputeChange(attribute))
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("nested_%s", name), 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),
|
||||
t.Run("object", 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: "single",
|
||||
},
|
||||
}
|
||||
return attributes
|
||||
}(),
|
||||
NestingMode: "single",
|
||||
},
|
||||
}
|
||||
|
||||
if tc.validateNestedObject != nil {
|
||||
tc.validateNestedObject(t, tc.input.ComputeChange(attribute))
|
||||
return
|
||||
}
|
||||
if tc.validateNestedObject != nil {
|
||||
tc.validateNestedObject(t, tc.input.ComputeChange(attribute))
|
||||
return
|
||||
}
|
||||
|
||||
if tc.validateSingleChange != nil {
|
||||
tc.validateSingleChange(t, tc.input.ComputeChange(attribute))
|
||||
return
|
||||
}
|
||||
if tc.validateSingleChange != nil {
|
||||
tc.validateSingleChange(t, tc.input.ComputeChange(attribute))
|
||||
return
|
||||
}
|
||||
|
||||
validate := change.ValidateNestedObject(tc.validateChanges, tc.validateAction, tc.validateReplace)
|
||||
validate(t, tc.input.ComputeChange(attribute))
|
||||
validate := change.ValidateNestedObject(tc.validateChanges, tc.validateAction, tc.validateReplace)
|
||||
validate(t, tc.input.ComputeChange(attribute))
|
||||
})
|
||||
|
||||
t.Run("map", 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: "map",
|
||||
},
|
||||
}
|
||||
|
||||
input := wrapValueInMap(tc.input)
|
||||
|
||||
if tc.validateNestedObject != nil {
|
||||
validate := change.ValidateMap(map[string]change.ValidateChangeFunc{
|
||||
"element": tc.validateNestedObject,
|
||||
}, collectionDefaultAction, false)
|
||||
validate(t, input.ComputeChange(attribute))
|
||||
return
|
||||
}
|
||||
|
||||
if tc.validateSingleChange != nil {
|
||||
validate := change.ValidateMap(map[string]change.ValidateChangeFunc{
|
||||
"element": tc.validateSingleChange,
|
||||
}, collectionDefaultAction, false)
|
||||
validate(t, input.ComputeChange(attribute))
|
||||
return
|
||||
}
|
||||
|
||||
validate := change.ValidateMap(map[string]change.ValidateChangeFunc{
|
||||
"element": change.ValidateNestedObject(tc.validateChanges, tc.validateAction, tc.validateReplace),
|
||||
}, collectionDefaultAction, false)
|
||||
validate(t, input.ComputeChange(attribute))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValue_Attribute(t *testing.T) {
|
||||
func TestValue_PrimitiveAttributes(t *testing.T) {
|
||||
// This function tests manipulating primitives: creating, deleting and
|
||||
// updating. It also automatically tests these operations within the
|
||||
// contexts of collections.
|
||||
|
||||
tcs := map[string]struct {
|
||||
input Value
|
||||
attribute *jsonprovider.Attribute
|
||||
attribute cty.Type
|
||||
validateChange change.ValidateChangeFunc
|
||||
}{
|
||||
"primitive_create": {
|
||||
input: Value{
|
||||
After: "new",
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
attribute: cty.String,
|
||||
validateChange: change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
|
||||
},
|
||||
"primitive_delete": {
|
||||
input: Value{
|
||||
Before: "old",
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
attribute: cty.String,
|
||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
},
|
||||
"primitive_update": {
|
||||
@ -443,9 +527,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
Before: "old",
|
||||
After: "new",
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
attribute: cty.String,
|
||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, false),
|
||||
},
|
||||
"primitive_set_explicit_null": {
|
||||
@ -454,9 +536,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
After: nil,
|
||||
AfterExplicit: true,
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
attribute: cty.String,
|
||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), nil, plans.Update, false),
|
||||
},
|
||||
"primitive_unset_explicit_null": {
|
||||
@ -465,9 +545,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
Before: nil,
|
||||
After: "new",
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
attribute: cty.String,
|
||||
validateChange: change.ValidatePrimitive(nil, strptr("\"new\""), plans.Update, false),
|
||||
},
|
||||
"primitive_create_sensitive": {
|
||||
@ -476,9 +554,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
After: "new",
|
||||
AfterSensitive: true,
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
attribute: cty.String,
|
||||
validateChange: change.ValidateSensitive(nil, "new", false, true, plans.Create, false),
|
||||
},
|
||||
"primitive_delete_sensitive": {
|
||||
@ -487,9 +563,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
BeforeSensitive: true,
|
||||
After: nil,
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
attribute: cty.String,
|
||||
validateChange: change.ValidateSensitive("old", nil, true, false, plans.Delete, false),
|
||||
},
|
||||
"primitive_update_sensitive": {
|
||||
@ -499,9 +573,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
After: "new",
|
||||
AfterSensitive: true,
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
attribute: cty.String,
|
||||
validateChange: change.ValidateSensitive("old", "new", true, true, plans.Update, false),
|
||||
},
|
||||
"primitive_create_computed": {
|
||||
@ -510,9 +582,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
After: nil,
|
||||
Unknown: true,
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
attribute: cty.String,
|
||||
validateChange: change.ValidateComputed(nil, plans.Create, false),
|
||||
},
|
||||
"primitive_update_computed": {
|
||||
@ -521,9 +591,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
After: nil,
|
||||
Unknown: true,
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
attribute: cty.String,
|
||||
validateChange: change.ValidateComputed(change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), plans.Update, false),
|
||||
},
|
||||
"primitive_update_replace": {
|
||||
@ -534,12 +602,171 @@ func TestValue_Attribute(t *testing.T) {
|
||||
[]interface{}{}, // An empty path suggests this attribute should be true.
|
||||
},
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.String),
|
||||
},
|
||||
attribute: cty.String,
|
||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, true),
|
||||
},
|
||||
"noop": {
|
||||
input: Value{
|
||||
Before: "old",
|
||||
After: "old",
|
||||
},
|
||||
attribute: cty.String,
|
||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"old\""), plans.NoOp, false),
|
||||
},
|
||||
}
|
||||
for name, tmp := range tcs {
|
||||
tc := tmp
|
||||
|
||||
defaultCollectionsAction := plans.Update
|
||||
if name == "noop" {
|
||||
defaultCollectionsAction = plans.NoOp
|
||||
}
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Run("direct", func(t *testing.T) {
|
||||
tc.validateChange(t, tc.input.ComputeChange(&jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, tc.attribute),
|
||||
}))
|
||||
})
|
||||
|
||||
t.Run("map", func(t *testing.T) {
|
||||
input := wrapValueInMap(tc.input)
|
||||
attribute := &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.Map(tc.attribute)),
|
||||
}
|
||||
|
||||
validate := change.ValidateMap(map[string]change.ValidateChangeFunc{
|
||||
"element": tc.validateChange,
|
||||
}, defaultCollectionsAction, false)
|
||||
validate(t, input.ComputeChange(attribute))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValue_CollectionAttributes(t *testing.T) {
|
||||
// This function tests creating and deleting collections. Note, it does not
|
||||
// generally cover editing collections except in special cases as editing
|
||||
// collections is handled automatically by other functions.
|
||||
tcs := map[string]struct {
|
||||
input Value
|
||||
attribute *jsonprovider.Attribute
|
||||
validateChange change.ValidateChangeFunc
|
||||
}{
|
||||
"map_create_empty": {
|
||||
input: Value{
|
||||
Before: nil,
|
||||
After: map[string]interface{}{},
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.Map(cty.String)),
|
||||
},
|
||||
validateChange: change.ValidateMap(nil, plans.Create, false),
|
||||
},
|
||||
"map_create_populated": {
|
||||
input: Value{
|
||||
Before: nil,
|
||||
After: map[string]interface{}{
|
||||
"element_one": "one",
|
||||
"element_two": "two",
|
||||
},
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.Map(cty.String)),
|
||||
},
|
||||
validateChange: change.ValidateMap(map[string]change.ValidateChangeFunc{
|
||||
"element_one": change.ValidatePrimitive(nil, strptr("\"one\""), plans.Create, false),
|
||||
"element_two": change.ValidatePrimitive(nil, strptr("\"two\""), plans.Create, false),
|
||||
}, plans.Create, false),
|
||||
},
|
||||
"map_delete_empty": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{},
|
||||
After: nil,
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.Map(cty.String)),
|
||||
},
|
||||
validateChange: change.ValidateMap(nil, plans.Delete, false),
|
||||
},
|
||||
"map_delete_populated": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"element_one": "one",
|
||||
"element_two": "two",
|
||||
},
|
||||
After: nil,
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.Map(cty.String)),
|
||||
},
|
||||
validateChange: change.ValidateMap(map[string]change.ValidateChangeFunc{
|
||||
"element_one": change.ValidatePrimitive(strptr("\"one\""), nil, plans.Delete, false),
|
||||
"element_two": change.ValidatePrimitive(strptr("\"two\""), nil, plans.Delete, false),
|
||||
}, plans.Delete, false),
|
||||
},
|
||||
"map_create_sensitive": {
|
||||
input: Value{
|
||||
Before: nil,
|
||||
After: map[string]interface{}{},
|
||||
AfterSensitive: true,
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.Map(cty.String)),
|
||||
},
|
||||
validateChange: change.ValidateSensitive(nil, map[string]interface{}{}, false, true, plans.Create, false),
|
||||
},
|
||||
"map_update_sensitive": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"element": "one",
|
||||
},
|
||||
BeforeSensitive: true,
|
||||
After: map[string]interface{}{},
|
||||
AfterSensitive: true,
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.Map(cty.String)),
|
||||
},
|
||||
validateChange: change.ValidateSensitive(map[string]interface{}{"element": "one"}, map[string]interface{}{}, true, true, plans.Update, false),
|
||||
},
|
||||
"map_delete_sensitive": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{},
|
||||
BeforeSensitive: true,
|
||||
After: nil,
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.Map(cty.String)),
|
||||
},
|
||||
validateChange: change.ValidateSensitive(map[string]interface{}{}, nil, true, false, plans.Delete, false),
|
||||
},
|
||||
"map_create_unknown": {
|
||||
input: Value{
|
||||
Before: nil,
|
||||
After: map[string]interface{}{},
|
||||
Unknown: true,
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.Map(cty.String)),
|
||||
},
|
||||
validateChange: change.ValidateComputed(nil, plans.Create, false),
|
||||
},
|
||||
"map_update_unknown": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{},
|
||||
After: map[string]interface{}{
|
||||
"element": "one",
|
||||
},
|
||||
Unknown: true,
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.Map(cty.String)),
|
||||
},
|
||||
validateChange: change.ValidateComputed(change.ValidateMap(nil, plans.Delete, false), plans.Update, false),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tcs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc.validateChange(t, tc.input.ComputeChange(tc.attribute))
|
||||
@ -547,6 +774,9 @@ func TestValue_Attribute(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// unmarshalType converts a cty.Type into a json.RawMessage understood by the
|
||||
// schema. It also lets the testing framework handle any errors to keep the API
|
||||
// clean.
|
||||
func unmarshalType(t *testing.T, ctyType cty.Type) json.RawMessage {
|
||||
msg, err := ctyjson.MarshalType(ctyType)
|
||||
if err != nil {
|
||||
@ -554,3 +784,42 @@ func unmarshalType(t *testing.T, ctyType cty.Type) json.RawMessage {
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// 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{} {
|
||||
switch value.(type) {
|
||||
case nil:
|
||||
if set, ok := unknown.(bool); (set && ok) || explicit {
|
||||
return map[string]interface{}{
|
||||
"element": nil,
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{}
|
||||
default:
|
||||
return map[string]interface{}{
|
||||
"element": 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),
|
||||
ReplacePaths: func() []interface{} {
|
||||
var ret []interface{}
|
||||
for _, path := range input.ReplacePaths {
|
||||
old := path.([]interface{})
|
||||
var updated []interface{}
|
||||
updated = append(updated, "element")
|
||||
updated = append(updated, old...)
|
||||
ret = append(ret, updated)
|
||||
}
|
||||
return ret
|
||||
}(),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user