mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Add support for object attributes in the structured renderer (#32391)
* 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
This commit is contained in:
parent
b8b1a8d430
commit
1eebcf875f
@ -1,6 +1,7 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
@ -60,6 +61,11 @@ func (change Change) Warnings(indent int) []string {
|
||||
return change.renderer.Warnings(change, indent)
|
||||
}
|
||||
|
||||
// GetAction returns the plans.Action that this change describes.
|
||||
func (change Change) GetAction() plans.Action {
|
||||
return change.action
|
||||
}
|
||||
|
||||
// nullSuffix returns the `-> null` suffix if the change is a delete action, and
|
||||
// it has not been overridden.
|
||||
func (change Change) nullSuffix(override bool) string {
|
||||
@ -83,3 +89,19 @@ func (change Change) forcesReplacement() string {
|
||||
func (change Change) indent(indent int) string {
|
||||
return strings.Repeat(" ", indent)
|
||||
}
|
||||
|
||||
// emptySymbol returns an empty string that is the same length as an action
|
||||
// symbol (eg. ' +', '+/-', ...). It is used to offset additional lines in
|
||||
// change renderer outputs alongside the indent function.
|
||||
func (change Change) emptySymbol() string {
|
||||
return " "
|
||||
}
|
||||
|
||||
// unchanged prints out a description saying how many of 'keyword' have been
|
||||
// hidden because they are unchanged or noop actions.
|
||||
func (change Change) unchanged(keyword string, count int) string {
|
||||
if count == 1 {
|
||||
return fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", count, keyword)
|
||||
}
|
||||
return fmt.Sprintf("[dark_gray]# (%d unchanged %ss hidden)[reset]", count, keyword)
|
||||
}
|
||||
|
88
internal/command/jsonformat/change/object.go
Normal file
88
internal/command/jsonformat/change/object.go
Normal file
@ -0,0 +1,88 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
func Object(attributes map[string]Change) Renderer {
|
||||
maximumKeyLen := 0
|
||||
for key := range attributes {
|
||||
if maximumKeyLen < len(key) {
|
||||
maximumKeyLen = len(key)
|
||||
}
|
||||
}
|
||||
|
||||
return &objectRenderer{
|
||||
attributes: attributes,
|
||||
maximumKeyLen: maximumKeyLen,
|
||||
overrideNullSuffix: true,
|
||||
}
|
||||
}
|
||||
|
||||
func NestedObject(attributes map[string]Change) Renderer {
|
||||
maximumKeyLen := 0
|
||||
for key := range attributes {
|
||||
if maximumKeyLen < len(key) {
|
||||
maximumKeyLen = len(key)
|
||||
}
|
||||
}
|
||||
|
||||
return &objectRenderer{
|
||||
attributes: attributes,
|
||||
maximumKeyLen: maximumKeyLen,
|
||||
overrideNullSuffix: false,
|
||||
}
|
||||
}
|
||||
|
||||
type objectRenderer struct {
|
||||
NoWarningsRenderer
|
||||
|
||||
attributes map[string]Change
|
||||
maximumKeyLen int
|
||||
overrideNullSuffix bool
|
||||
}
|
||||
|
||||
func (renderer objectRenderer) Render(change Change, indent int, opts RenderOpts) string {
|
||||
if len(renderer.attributes) == 0 {
|
||||
return fmt.Sprintf("{}%s%s", change.nullSuffix(opts.overrideNullSuffix), change.forcesReplacement())
|
||||
}
|
||||
|
||||
attributeOpts := opts.Clone()
|
||||
attributeOpts.overrideNullSuffix = renderer.overrideNullSuffix
|
||||
|
||||
var keys []string
|
||||
for key := range renderer.attributes {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
unchangedAttributes := 0
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(fmt.Sprintf("{%s\n", change.forcesReplacement()))
|
||||
for _, key := range keys {
|
||||
attribute := renderer.attributes[key]
|
||||
|
||||
if attribute.action == plans.NoOp && !opts.showUnchangedChildren {
|
||||
// Don't render NoOp operations when we are compact display.
|
||||
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, attributeOpts)))
|
||||
}
|
||||
|
||||
if unchangedAttributes > 0 {
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s\n", change.indent(indent+1), change.emptySymbol(), change.unchanged("attribute", unchangedAttributes)))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("%s%s }%s", change.indent(indent), change.emptySymbol(), change.nullSuffix(opts.overrideNullSuffix)))
|
||||
return buf.String()
|
||||
}
|
@ -32,6 +32,14 @@ type RenderOpts struct {
|
||||
// change, as such we provide this as an option instead of trying to
|
||||
// calculate it inside a specific renderer.
|
||||
overrideNullSuffix bool
|
||||
|
||||
// showUnchangedChildren instructs the Renderer to render all children of a
|
||||
// given complex change, instead of hiding unchanged items and compressing
|
||||
// them into a single line.
|
||||
//
|
||||
// This is generally decided by the parent change (mainly lists) and so is
|
||||
// passed in as a private option.
|
||||
showUnchangedChildren bool
|
||||
}
|
||||
|
||||
// Clone returns a new RenderOpts object, that matches the original but can be
|
||||
|
@ -1,6 +1,7 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@ -107,12 +108,256 @@ func TestRenderers(t *testing.T) {
|
||||
},
|
||||
expected: "0 -> (known after apply)",
|
||||
},
|
||||
"object_created": {
|
||||
change: Change{
|
||||
renderer: Object(map[string]Change{}),
|
||||
action: plans.Create,
|
||||
},
|
||||
expected: "{}",
|
||||
},
|
||||
"object_created_with_attributes": {
|
||||
change: Change{
|
||||
renderer: Object(map[string]Change{
|
||||
"attribute_one": {
|
||||
renderer: Primitive(nil, strptr("0")),
|
||||
action: plans.Create,
|
||||
},
|
||||
}),
|
||||
action: plans.Create,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
+ attribute_one = 0
|
||||
}
|
||||
`,
|
||||
},
|
||||
"object_deleted": {
|
||||
change: Change{
|
||||
renderer: Object(map[string]Change{}),
|
||||
action: plans.Delete,
|
||||
},
|
||||
expected: "{} -> null",
|
||||
},
|
||||
"object_deleted_with_attributes": {
|
||||
change: Change{
|
||||
renderer: Object(map[string]Change{
|
||||
"attribute_one": {
|
||||
renderer: Primitive(strptr("0"), nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
}),
|
||||
action: plans.Delete,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
- attribute_one = 0
|
||||
} -> null
|
||||
`,
|
||||
},
|
||||
"nested_object_deleted": {
|
||||
change: Change{
|
||||
renderer: NestedObject(map[string]Change{}),
|
||||
action: plans.Delete,
|
||||
},
|
||||
expected: "{} -> null",
|
||||
},
|
||||
"nested_object_deleted_with_attributes": {
|
||||
change: Change{
|
||||
renderer: NestedObject(map[string]Change{
|
||||
"attribute_one": {
|
||||
renderer: Primitive(strptr("0"), nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
}),
|
||||
action: plans.Delete,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
- attribute_one = 0 -> null
|
||||
} -> null
|
||||
`,
|
||||
},
|
||||
"object_create_attribute": {
|
||||
change: Change{
|
||||
renderer: Object(map[string]Change{
|
||||
"attribute_one": {
|
||||
renderer: Primitive(nil, strptr("0")),
|
||||
action: plans.Create,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
+ attribute_one = 0
|
||||
}
|
||||
`,
|
||||
},
|
||||
"object_update_attribute": {
|
||||
change: Change{
|
||||
renderer: Object(map[string]Change{
|
||||
"attribute_one": {
|
||||
renderer: Primitive(strptr("0"), strptr("1")),
|
||||
action: plans.Update,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
~ attribute_one = 0 -> 1
|
||||
}
|
||||
`,
|
||||
},
|
||||
"object_update_attribute_forces_replacement": {
|
||||
change: Change{
|
||||
renderer: Object(map[string]Change{
|
||||
"attribute_one": {
|
||||
renderer: Primitive(strptr("0"), strptr("1")),
|
||||
action: plans.Update,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
replace: true,
|
||||
},
|
||||
expected: `
|
||||
{ # forces replacement
|
||||
~ attribute_one = 0 -> 1
|
||||
}
|
||||
`,
|
||||
},
|
||||
"object_delete_attribute": {
|
||||
change: Change{
|
||||
renderer: Object(map[string]Change{
|
||||
"attribute_one": {
|
||||
renderer: Primitive(strptr("0"), nil),
|
||||
action: plans.Delete,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
- attribute_one = 0
|
||||
}
|
||||
`,
|
||||
},
|
||||
"object_ignore_unchanged_attributes": {
|
||||
change: Change{
|
||||
renderer: Object(map[string]Change{
|
||||
"attribute_one": {
|
||||
renderer: Primitive(strptr("0"), strptr("1")),
|
||||
action: plans.Update,
|
||||
},
|
||||
"attribute_two": {
|
||||
renderer: Primitive(strptr("0"), strptr("0")),
|
||||
action: plans.NoOp,
|
||||
},
|
||||
"attribute_three": {
|
||||
renderer: Primitive(nil, strptr("1")),
|
||||
action: plans.Create,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
~ attribute_one = 0 -> 1
|
||||
+ attribute_three = 1
|
||||
# (1 unchanged attribute hidden)
|
||||
}
|
||||
`,
|
||||
},
|
||||
"object_create_sensitive_attribute": {
|
||||
change: Change{
|
||||
renderer: Object(map[string]Change{
|
||||
"attribute_one": {
|
||||
renderer: Sensitive(nil, 1, false, true),
|
||||
action: plans.Create,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
+ attribute_one = (sensitive)
|
||||
}
|
||||
`,
|
||||
},
|
||||
"object_update_sensitive_attribute": {
|
||||
change: Change{
|
||||
renderer: Object(map[string]Change{
|
||||
"attribute_one": {
|
||||
renderer: Sensitive(nil, 1, false, true),
|
||||
action: plans.Update,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
~ attribute_one = (sensitive)
|
||||
}
|
||||
`,
|
||||
},
|
||||
"object_delete_sensitive_attribute": {
|
||||
change: Change{
|
||||
renderer: Object(map[string]Change{
|
||||
"attribute_one": {
|
||||
renderer: Sensitive(nil, 1, false, true),
|
||||
action: plans.Delete,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
- attribute_one = (sensitive)
|
||||
}
|
||||
`,
|
||||
},
|
||||
"object_create_computed_attribute": {
|
||||
change: Change{
|
||||
renderer: Object(map[string]Change{
|
||||
"attribute_one": {
|
||||
renderer: Computed(Change{renderer: nil}),
|
||||
action: plans.Create,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
+ attribute_one = (known after apply)
|
||||
}
|
||||
`,
|
||||
},
|
||||
"object_update_computed_attribute": {
|
||||
change: Change{
|
||||
renderer: Object(map[string]Change{
|
||||
"attribute_one": {
|
||||
renderer: Computed(Change{
|
||||
renderer: Primitive(strptr("1"), nil),
|
||||
action: plans.Delete,
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
}),
|
||||
action: plans.Update,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
~ attribute_one = 1 -> (known after apply)
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
for name, tc := range tcs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
expected := strings.TrimSpace(tc.expected)
|
||||
actual := colorize.Color(tc.change.Render(0, tc.opts))
|
||||
if diff := cmp.Diff(tc.expected, actual); len(diff) > 0 {
|
||||
t.Fatalf("\nexpected:\n%s\nactual:\n%s\ndiff:\n%s\n", tc.expected, actual, diff)
|
||||
if diff := cmp.Diff(expected, actual); len(diff) > 0 {
|
||||
t.Fatalf("\nexpected:\n%s\nactual:\n%s\ndiff:\n%s\n", expected, actual, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@ -10,16 +11,16 @@ import (
|
||||
|
||||
type ValidateChangeFunc func(t *testing.T, change Change)
|
||||
|
||||
func ValidateChange(t *testing.T, f ValidateChangeFunc, change Change, expectedAction plans.Action, expectedReplace bool) {
|
||||
func validateChange(t *testing.T, change Change, expectedAction plans.Action, expectedReplace bool) {
|
||||
if change.replace != expectedReplace || change.action != expectedAction {
|
||||
t.Fatalf("\nreplace:\n\texpected:%t\n\tactual:%t\naction:\n\texpected:%s\n\tactual:%s", expectedReplace, change.replace, expectedAction, change.action)
|
||||
}
|
||||
|
||||
f(t, change)
|
||||
}
|
||||
|
||||
func ValidatePrimitive(before, after *string) ValidateChangeFunc {
|
||||
func ValidatePrimitive(before, after *string, action plans.Action, replace bool) ValidateChangeFunc {
|
||||
return func(t *testing.T, change Change) {
|
||||
validateChange(t, change, action, replace)
|
||||
|
||||
primitive, ok := change.renderer.(*primitiveRenderer)
|
||||
if !ok {
|
||||
t.Fatalf("invalid renderer type: %T", change.renderer)
|
||||
@ -34,8 +35,68 @@ func ValidatePrimitive(before, after *string) ValidateChangeFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateSensitive(before, after interface{}, beforeSensitive, afterSensitive bool) ValidateChangeFunc {
|
||||
func ValidateObject(attributes map[string]ValidateChangeFunc, action plans.Action, replace bool) ValidateChangeFunc {
|
||||
return func(t *testing.T, change Change) {
|
||||
validateChange(t, change, action, replace)
|
||||
|
||||
object, ok := change.renderer.(*objectRenderer)
|
||||
if !ok {
|
||||
t.Fatalf("invalid renderer type: %T", change.renderer)
|
||||
}
|
||||
|
||||
if !object.overrideNullSuffix {
|
||||
t.Fatalf("created the wrong type of object renderer")
|
||||
}
|
||||
|
||||
validateObject(t, object, attributes)
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateNestedObject(attributes map[string]ValidateChangeFunc, action plans.Action, replace bool) ValidateChangeFunc {
|
||||
return func(t *testing.T, change Change) {
|
||||
validateChange(t, change, action, replace)
|
||||
|
||||
object, ok := change.renderer.(*objectRenderer)
|
||||
if !ok {
|
||||
t.Fatalf("invalid renderer type: %T", change.renderer)
|
||||
}
|
||||
|
||||
if object.overrideNullSuffix {
|
||||
t.Fatalf("created the wrong type of object renderer")
|
||||
}
|
||||
|
||||
validateObject(t, object, attributes)
|
||||
}
|
||||
}
|
||||
|
||||
func validateObject(t *testing.T, object *objectRenderer, attributes map[string]ValidateChangeFunc) {
|
||||
if len(object.attributes) != len(attributes) {
|
||||
t.Fatalf("expected %d attributes but found %d attributes", len(attributes), len(object.attributes))
|
||||
}
|
||||
|
||||
var missing []string
|
||||
for key, expected := range attributes {
|
||||
actual, ok := object.attributes[key]
|
||||
if !ok {
|
||||
missing = append(missing, key)
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
expected(t, actual)
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
t.Fatalf("missing the following attributes: %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)
|
||||
|
||||
sensitive, ok := change.renderer.(*sensitiveRenderer)
|
||||
if !ok {
|
||||
t.Fatalf("invalid renderer type: %T", change.renderer)
|
||||
@ -54,8 +115,10 @@ func ValidateSensitive(before, after interface{}, beforeSensitive, afterSensitiv
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateComputed(before ValidateChangeFunc) ValidateChangeFunc {
|
||||
func ValidateComputed(before ValidateChangeFunc, action plans.Action, replace bool) ValidateChangeFunc {
|
||||
return func(t *testing.T, change Change) {
|
||||
validateChange(t, change, action, replace)
|
||||
|
||||
computed, ok := change.renderer.(*computedRenderer)
|
||||
if !ok {
|
||||
t.Fatalf("invalid renderer type: %T", change.renderer)
|
||||
|
@ -8,37 +8,34 @@ import (
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
)
|
||||
|
||||
func (v Value) ComputeChangeForAttribute(attribute *jsonprovider.Attribute) change.Change {
|
||||
return v.ComputeChangeForType(unmarshalAttribute(attribute))
|
||||
func (v Value) computeChangeForAttribute(attribute *jsonprovider.Attribute) change.Change {
|
||||
if attribute.AttributeNestedType != nil {
|
||||
return v.computeChangeForNestedAttribute(attribute.AttributeNestedType)
|
||||
}
|
||||
return v.computeChangeForType(unmarshalAttribute(attribute))
|
||||
}
|
||||
|
||||
func (v Value) ComputeChangeForType(ctyType cty.Type) change.Change {
|
||||
|
||||
if sensitive, ok := v.checkForSensitive(); ok {
|
||||
return sensitive
|
||||
}
|
||||
|
||||
if computed, ok := v.checkForComputed(ctyType); ok {
|
||||
return computed
|
||||
func (v Value) computeChangeForNestedAttribute(attribute *jsonprovider.NestedType) change.Change {
|
||||
switch attribute.NestingMode {
|
||||
case "single", "group":
|
||||
return v.computeAttributeChangeAsNestedObject(attribute.Attributes)
|
||||
default:
|
||||
panic("unrecognized nesting mode: " + attribute.NestingMode)
|
||||
}
|
||||
}
|
||||
|
||||
func (v Value) computeChangeForType(ctyType cty.Type) change.Change {
|
||||
switch {
|
||||
case ctyType.IsPrimitiveType():
|
||||
return v.computeAttributeChangeAsPrimitive(ctyType)
|
||||
case ctyType.IsObjectType():
|
||||
return v.computeAttributeChangeAsObject(ctyType.AttributeTypes())
|
||||
default:
|
||||
panic("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalAttribute(attribute *jsonprovider.Attribute) cty.Type {
|
||||
if attribute.AttributeNestedType != nil {
|
||||
children := make(map[string]cty.Type)
|
||||
for key, child := range attribute.AttributeNestedType.Attributes {
|
||||
children[key] = unmarshalAttribute(child)
|
||||
}
|
||||
return cty.Object(children)
|
||||
}
|
||||
|
||||
ctyType, err := ctyjson.UnmarshalType(attribute.AttributeType)
|
||||
if err != nil {
|
||||
panic("could not unmarshal attribute type: " + err.Error())
|
||||
|
@ -1,12 +1,10 @@
|
||||
package differ
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/change"
|
||||
)
|
||||
|
||||
func (v Value) checkForComputed(ctyType cty.Type) (change.Change, bool) {
|
||||
func (v Value) checkForComputed(changeType interface{}) (change.Change, bool) {
|
||||
unknown := v.isUnknown()
|
||||
|
||||
if !unknown {
|
||||
@ -30,7 +28,7 @@ func (v Value) checkForComputed(ctyType cty.Type) (change.Change, bool) {
|
||||
Before: v.Before,
|
||||
BeforeSensitive: v.BeforeSensitive,
|
||||
}
|
||||
return v.AsChange(change.Computed(beforeValue.ComputeChangeForType(ctyType))), true
|
||||
return v.AsChange(change.Computed(beforeValue.ComputeChange(changeType))), true
|
||||
}
|
||||
|
||||
func (v Value) isUnknown() bool {
|
||||
|
61
internal/command/jsonformat/differ/object.go
Normal file
61
internal/command/jsonformat/differ/object.go
Normal file
@ -0,0 +1,61 @@
|
||||
package differ
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/change"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
func (v Value) computeAttributeChangeAsObject(attributes map[string]cty.Type) change.Change {
|
||||
var keys []string
|
||||
for key := range attributes {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
attributeChanges, changeType := v.processObject(keys, func(key string) interface{} {
|
||||
return attributes[key]
|
||||
})
|
||||
return change.New(change.Object(attributeChanges), changeType, v.replacePath())
|
||||
}
|
||||
|
||||
func (v Value) computeAttributeChangeAsNestedObject(attributes map[string]*jsonprovider.Attribute) change.Change {
|
||||
var keys []string
|
||||
for key := range attributes {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
attributeChanges, changeType := v.processObject(keys, func(key string) interface{} {
|
||||
return attributes[key]
|
||||
})
|
||||
return change.New(change.NestedObject(attributeChanges), changeType, v.replacePath())
|
||||
}
|
||||
|
||||
func (v Value) processObject(keys []string, getAttribute func(string) interface{}) (map[string]change.Change, plans.Action) {
|
||||
attributeChanges := make(map[string]change.Change)
|
||||
mapValue := v.asMap()
|
||||
|
||||
currentAction := v.getDefaultActionForIteration()
|
||||
for _, key := range keys {
|
||||
attribute := getAttribute(key)
|
||||
attributeValue := mapValue.getChild(key)
|
||||
|
||||
// We always assume changes to object are implicit.
|
||||
attributeValue.BeforeExplicit = false
|
||||
attributeValue.AfterExplicit = false
|
||||
|
||||
// We use the generic ComputeChange here, as we don't know whether this
|
||||
// is from a nested object or a `normal` object.
|
||||
attributeChange := attributeValue.ComputeChange(attribute)
|
||||
if attributeChange.GetAction() == plans.NoOp && attributeValue.Before == nil && attributeValue.After == nil {
|
||||
// We skip attributes of objects that are null both before and
|
||||
// after. We don't even count these as unchanged attributes.
|
||||
continue
|
||||
}
|
||||
attributeChanges[key] = attributeChange
|
||||
currentAction = compareActions(currentAction, attributeChange.GetAction())
|
||||
}
|
||||
|
||||
return attributeChanges, currentAction
|
||||
}
|
@ -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
|
||||
@ -99,16 +99,26 @@ func ValueFromJsonChange(change jsonplan.Change) Value {
|
||||
}
|
||||
|
||||
// ComputeChange is a generic function that lets callers no worry about what
|
||||
// type of change they are processing.
|
||||
// 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.
|
||||
//
|
||||
// It can accept blocks, attributes, go-cty types, and outputs, and will route
|
||||
// the request to the appropriate function.
|
||||
func (v Value) ComputeChange(changeType interface{}) change.Change {
|
||||
if sensitive, ok := v.checkForSensitive(); ok {
|
||||
return sensitive
|
||||
}
|
||||
|
||||
if computed, ok := v.checkForComputed(changeType); ok {
|
||||
return computed
|
||||
}
|
||||
|
||||
switch concrete := changeType.(type) {
|
||||
case *jsonprovider.Attribute:
|
||||
return v.ComputeChangeForAttribute(concrete)
|
||||
return v.computeChangeForAttribute(concrete)
|
||||
case cty.Type:
|
||||
return v.ComputeChangeForType(concrete)
|
||||
return v.computeChangeForType(concrete)
|
||||
default:
|
||||
panic(fmt.Sprintf("unrecognized change type: %T", changeType))
|
||||
}
|
||||
@ -140,6 +150,43 @@ func (v Value) calculateChange() plans.Action {
|
||||
return plans.Update
|
||||
}
|
||||
|
||||
// getDefaultActionForIteration is used to guess what the change could be for
|
||||
// complex attributes (collections and objects) and blocks.
|
||||
//
|
||||
// You can't really tell the difference between a NoOp and an Update just by
|
||||
// looking at the attribute itself as you need to inspect the children.
|
||||
//
|
||||
// This function returns a Delete or a Create action if the before or after
|
||||
// values were null, and returns a NoOp for all other cases. It should be used
|
||||
// in conjunction with compareActions to calculate the actual action based on
|
||||
// the actions of the children.
|
||||
func (v Value) getDefaultActionForIteration() plans.Action {
|
||||
if v.Before == nil && v.After == nil {
|
||||
return plans.NoOp
|
||||
}
|
||||
|
||||
if v.Before == nil {
|
||||
return plans.Create
|
||||
}
|
||||
if v.After == nil {
|
||||
return plans.Delete
|
||||
}
|
||||
return plans.NoOp
|
||||
}
|
||||
|
||||
// compareActions will compare current and next, and return plans.Update if they
|
||||
// are different, and current if they are the same.
|
||||
//
|
||||
// This function should be used in conjunction with getDefaultActionForIteration
|
||||
// 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 current != next {
|
||||
return plans.Update
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func unmarshalGeneric(raw json.RawMessage) interface{} {
|
||||
if raw == nil {
|
||||
return nil
|
||||
|
69
internal/command/jsonformat/differ/value_map.go
Normal file
69
internal/command/jsonformat/differ/value_map.go
Normal file
@ -0,0 +1,69 @@
|
||||
package differ
|
||||
|
||||
// ValueMap is a Value that represents a Map or an Object type, and has
|
||||
// converted the relevant interfaces into maps for easier access.
|
||||
type ValueMap struct {
|
||||
// Before contains the value before the proposed change.
|
||||
Before map[string]interface{}
|
||||
|
||||
// After contains the value after the proposed change.
|
||||
After map[string]interface{}
|
||||
|
||||
// Unknown contains the unknown status of any elements/attributes of this
|
||||
// map/object.
|
||||
Unknown map[string]interface{}
|
||||
|
||||
// BeforeSensitive contains the before sensitive status of any
|
||||
// elements/attributes of this map/object.
|
||||
BeforeSensitive map[string]interface{}
|
||||
|
||||
// AfterSensitive contains the after sensitive status of any
|
||||
// elements/attributes of this map/object.
|
||||
AfterSensitive map[string]interface{}
|
||||
}
|
||||
|
||||
func (v Value) asMap() ValueMap {
|
||||
return ValueMap{
|
||||
Before: genericToMap(v.Before),
|
||||
After: genericToMap(v.After),
|
||||
Unknown: genericToMap(v.Unknown),
|
||||
BeforeSensitive: genericToMap(v.BeforeSensitive),
|
||||
AfterSensitive: genericToMap(v.AfterSensitive),
|
||||
}
|
||||
}
|
||||
|
||||
func (m ValueMap) getChild(key string) Value {
|
||||
before, beforeExplicit := getFromGenericMap(m.Before, key)
|
||||
after, afterExplicit := getFromGenericMap(m.After, key)
|
||||
unknown, _ := getFromGenericMap(m.Unknown, key)
|
||||
beforeSensitive, _ := getFromGenericMap(m.BeforeSensitive, key)
|
||||
afterSensitive, _ := getFromGenericMap(m.AfterSensitive, key)
|
||||
|
||||
return Value{
|
||||
BeforeExplicit: beforeExplicit,
|
||||
AfterExplicit: afterExplicit,
|
||||
Before: before,
|
||||
After: after,
|
||||
Unknown: unknown,
|
||||
BeforeSensitive: beforeSensitive,
|
||||
AfterSensitive: afterSensitive,
|
||||
}
|
||||
}
|
||||
|
||||
func getFromGenericMap(generic map[string]interface{}, key string) (interface{}, bool) {
|
||||
if generic == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if child, ok := generic[key]; ok {
|
||||
return child, ok
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func genericToMap(generic interface{}) map[string]interface{} {
|
||||
if concrete, ok := generic.(map[string]interface{}); ok {
|
||||
return concrete
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,20 +1,382 @@
|
||||
package differ
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/change"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
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.
|
||||
|
||||
tcs := map[string]struct {
|
||||
input Value
|
||||
attributes map[string]cty.Type
|
||||
validateSingleChange change.ValidateChangeFunc
|
||||
validateObject change.ValidateChangeFunc
|
||||
validateNestedObject change.ValidateChangeFunc
|
||||
validateChanges map[string]change.ValidateChangeFunc
|
||||
validateReplace bool
|
||||
validateAction plans.Action
|
||||
}{
|
||||
"object_create": {
|
||||
input: Value{
|
||||
Before: nil,
|
||||
After: map[string]interface{}{
|
||||
"attribute_one": "new",
|
||||
},
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateChanges: map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
|
||||
},
|
||||
validateAction: plans.Create,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_delete": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
},
|
||||
After: nil,
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateChanges: map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
},
|
||||
validateAction: plans.Delete,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_create_sensitive": {
|
||||
input: Value{
|
||||
Before: nil,
|
||||
After: map[string]interface{}{
|
||||
"attribute_one": "new",
|
||||
},
|
||||
AfterSensitive: true,
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateSingleChange: change.ValidateSensitive(nil, map[string]interface{}{
|
||||
"attribute_one": "new",
|
||||
}, false, true, plans.Create, false),
|
||||
},
|
||||
"object_delete_sensitive": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
},
|
||||
BeforeSensitive: true,
|
||||
After: nil,
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateSingleChange: change.ValidateSensitive(map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
}, nil, true, false, plans.Delete, false),
|
||||
},
|
||||
"object_create_unknown": {
|
||||
input: Value{
|
||||
Before: nil,
|
||||
After: nil,
|
||||
Unknown: true,
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateSingleChange: change.ValidateComputed(nil, plans.Create, false),
|
||||
},
|
||||
"object_update_unknown": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
},
|
||||
After: nil,
|
||||
Unknown: true,
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateObject: change.ValidateComputed(change.ValidateObject(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
}, plans.Delete, false), plans.Update, false),
|
||||
validateNestedObject: change.ValidateComputed(change.ValidateNestedObject(map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
}, plans.Delete, false), plans.Update, false),
|
||||
},
|
||||
"object_create_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{},
|
||||
After: map[string]interface{}{
|
||||
"attribute_one": "new",
|
||||
},
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateChanges: map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
|
||||
},
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_create_attribute_from_explicit_null": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": nil,
|
||||
},
|
||||
After: map[string]interface{}{
|
||||
"attribute_one": "new",
|
||||
},
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateChanges: map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
|
||||
},
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_delete_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
},
|
||||
After: map[string]interface{}{},
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateChanges: map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
},
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_delete_attribute_to_explicit_null": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
},
|
||||
After: map[string]interface{}{
|
||||
"attribute_one": nil,
|
||||
},
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateChanges: map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
},
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_update_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
},
|
||||
After: map[string]interface{}{
|
||||
"attribute_one": "new",
|
||||
},
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateChanges: map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, false),
|
||||
},
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_create_sensitive_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{},
|
||||
After: map[string]interface{}{
|
||||
"attribute_one": "new",
|
||||
},
|
||||
AfterSensitive: map[string]interface{}{
|
||||
"attribute_one": true,
|
||||
},
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateChanges: map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidateSensitive(nil, "new", false, true, plans.Create, false),
|
||||
},
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_delete_sensitive_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
},
|
||||
BeforeSensitive: map[string]interface{}{
|
||||
"attribute_one": true,
|
||||
},
|
||||
After: map[string]interface{}{},
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateChanges: map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidateSensitive("old", nil, true, false, plans.Delete, false),
|
||||
},
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_update_sensitive_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
},
|
||||
BeforeSensitive: map[string]interface{}{
|
||||
"attribute_one": true,
|
||||
},
|
||||
After: map[string]interface{}{
|
||||
"attribute_one": "new",
|
||||
},
|
||||
AfterSensitive: map[string]interface{}{
|
||||
"attribute_one": true,
|
||||
},
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateChanges: map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidateSensitive("old", "new", true, true, plans.Update, false),
|
||||
},
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_create_computed_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{},
|
||||
After: map[string]interface{}{},
|
||||
Unknown: map[string]interface{}{
|
||||
"attribute_one": true,
|
||||
},
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateChanges: map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidateComputed(nil, plans.Create, false),
|
||||
},
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_update_computed_attribute": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{
|
||||
"attribute_one": "old",
|
||||
},
|
||||
After: map[string]interface{}{},
|
||||
Unknown: map[string]interface{}{
|
||||
"attribute_one": true,
|
||||
},
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateChanges: map[string]change.ValidateChangeFunc{
|
||||
"attribute_one": change.ValidateComputed(
|
||||
change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
plans.Update,
|
||||
false),
|
||||
},
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
},
|
||||
"object_ignores_unset_fields": {
|
||||
input: Value{
|
||||
Before: map[string]interface{}{},
|
||||
After: map[string]interface{}{},
|
||||
},
|
||||
attributes: map[string]cty.Type{
|
||||
"attribute_one": cty.String,
|
||||
},
|
||||
validateChanges: map[string]change.ValidateChangeFunc{},
|
||||
validateAction: plans.NoOp,
|
||||
validateReplace: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tcs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
||||
attribute := &jsonprovider.Attribute{
|
||||
AttributeType: unmarshalType(t, cty.Object(tc.attributes)),
|
||||
}
|
||||
|
||||
if tc.validateObject != nil {
|
||||
tc.validateObject(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(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),
|
||||
}
|
||||
}
|
||||
return attributes
|
||||
}(),
|
||||
NestingMode: "single",
|
||||
},
|
||||
}
|
||||
|
||||
if tc.validateNestedObject != nil {
|
||||
tc.validateNestedObject(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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValue_Attribute(t *testing.T) {
|
||||
tcs := map[string]struct {
|
||||
input Value
|
||||
attribute *jsonprovider.Attribute
|
||||
expectedAction plans.Action
|
||||
expectedReplace bool
|
||||
validateChange change.ValidateChangeFunc
|
||||
input Value
|
||||
attribute *jsonprovider.Attribute
|
||||
validateChange change.ValidateChangeFunc
|
||||
}{
|
||||
"primitive_create": {
|
||||
input: Value{
|
||||
@ -23,8 +385,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Create,
|
||||
validateChange: change.ValidatePrimitive(nil, strptr("\"new\"")),
|
||||
validateChange: change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
|
||||
},
|
||||
"primitive_delete": {
|
||||
input: Value{
|
||||
@ -33,8 +394,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Delete,
|
||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), nil),
|
||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
|
||||
},
|
||||
"primitive_update": {
|
||||
input: Value{
|
||||
@ -44,8 +404,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Update,
|
||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\"")),
|
||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, false),
|
||||
},
|
||||
"primitive_set_explicit_null": {
|
||||
input: Value{
|
||||
@ -56,8 +415,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Update,
|
||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), nil),
|
||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), nil, plans.Update, false),
|
||||
},
|
||||
"primitive_unset_explicit_null": {
|
||||
input: Value{
|
||||
@ -68,8 +426,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Update,
|
||||
validateChange: change.ValidatePrimitive(nil, strptr("\"new\"")),
|
||||
validateChange: change.ValidatePrimitive(nil, strptr("\"new\""), plans.Update, false),
|
||||
},
|
||||
"primitive_create_sensitive": {
|
||||
input: Value{
|
||||
@ -80,8 +437,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Create,
|
||||
validateChange: change.ValidateSensitive(nil, "new", false, true),
|
||||
validateChange: change.ValidateSensitive(nil, "new", false, true, plans.Create, false),
|
||||
},
|
||||
"primitive_delete_sensitive": {
|
||||
input: Value{
|
||||
@ -92,8 +448,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Delete,
|
||||
validateChange: change.ValidateSensitive("old", nil, true, false),
|
||||
validateChange: change.ValidateSensitive("old", nil, true, false, plans.Delete, false),
|
||||
},
|
||||
"primitive_update_sensitive": {
|
||||
input: Value{
|
||||
@ -105,8 +460,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Update,
|
||||
validateChange: change.ValidateSensitive("old", "new", true, true),
|
||||
validateChange: change.ValidateSensitive("old", "new", true, true, plans.Update, false),
|
||||
},
|
||||
"primitive_create_computed": {
|
||||
input: Value{
|
||||
@ -117,8 +471,7 @@ func TestValue_Attribute(t *testing.T) {
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Create,
|
||||
validateChange: change.ValidateComputed(nil),
|
||||
validateChange: change.ValidateComputed(nil, plans.Create, false),
|
||||
},
|
||||
"primitive_update_computed": {
|
||||
input: Value{
|
||||
@ -129,18 +482,20 @@ func TestValue_Attribute(t *testing.T) {
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Update,
|
||||
validateChange: change.ValidateComputed(change.ValidatePrimitive(strptr("\"old\""), nil)),
|
||||
validateChange: change.ValidateComputed(change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false), plans.Update, false),
|
||||
},
|
||||
}
|
||||
for name, tc := range tcs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
change.ValidateChange(
|
||||
t,
|
||||
tc.validateChange,
|
||||
tc.input.ComputeChangeForAttribute(tc.attribute),
|
||||
tc.expectedAction,
|
||||
tc.expectedReplace)
|
||||
tc.validateChange(t, tc.input.ComputeChange(tc.attribute))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalType(t *testing.T, ctyType cty.Type) json.RawMessage {
|
||||
msg, err := ctyjson.MarshalType(ctyType)
|
||||
if err != nil {
|
||||
t.Fatalf("invalid type: %s", ctyType.FriendlyName())
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user