Add support for unknown/computed values in the structured renderer (#32378)

* 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
This commit is contained in:
Liam Cervante 2023-01-09 11:55:55 +01:00 committed by GitHub
parent 6ab277f6ba
commit b8b1a8d430
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 159 additions and 37 deletions

View File

@ -0,0 +1,29 @@
package change
import (
"fmt"
"github.com/hashicorp/terraform/internal/plans"
)
func Computed(before Change) Renderer {
return &computedRenderer{
before: before,
}
}
type computedRenderer struct {
NoWarningsRenderer
before Change
}
func (renderer computedRenderer) Render(change Change, indent int, opts RenderOpts) string {
if change.action == plans.Create {
return "(known after apply)"
}
// Never render null suffix for children of computed changes.
opts.overrideNullSuffix = true
return fmt.Sprintf("%s -> (known after apply)", renderer.before.Render(indent, opts))
}

View File

@ -7,7 +7,7 @@ import (
) )
func Primitive(before, after *string) Renderer { func Primitive(before, after *string) Renderer {
return primitiveRenderer{ return &primitiveRenderer{
before: before, before: before,
after: after, after: after,
} }

View File

@ -28,7 +28,6 @@ func TestRenderers(t *testing.T) {
change: Change{ change: Change{
renderer: Primitive(nil, strptr("1")), renderer: Primitive(nil, strptr("1")),
action: plans.Create, action: plans.Create,
replace: false,
}, },
expected: "1", expected: "1",
}, },
@ -36,7 +35,6 @@ func TestRenderers(t *testing.T) {
change: Change{ change: Change{
renderer: Primitive(strptr("1"), nil), renderer: Primitive(strptr("1"), nil),
action: plans.Delete, action: plans.Delete,
replace: false,
}, },
expected: "1 -> null", expected: "1 -> null",
}, },
@ -44,7 +42,6 @@ func TestRenderers(t *testing.T) {
change: Change{ change: Change{
renderer: Primitive(strptr("1"), nil), renderer: Primitive(strptr("1"), nil),
action: plans.Delete, action: plans.Delete,
replace: false,
}, },
opts: RenderOpts{overrideNullSuffix: true}, opts: RenderOpts{overrideNullSuffix: true},
expected: "1", expected: "1",
@ -53,7 +50,6 @@ func TestRenderers(t *testing.T) {
change: Change{ change: Change{
renderer: Primitive(strptr("1"), nil), renderer: Primitive(strptr("1"), nil),
action: plans.Update, action: plans.Update,
replace: false,
}, },
expected: "1 -> null", expected: "1 -> null",
}, },
@ -61,7 +57,6 @@ func TestRenderers(t *testing.T) {
change: Change{ change: Change{
renderer: Primitive(nil, strptr("1")), renderer: Primitive(nil, strptr("1")),
action: plans.Update, action: plans.Update,
replace: false,
}, },
expected: "null -> 1", expected: "null -> 1",
}, },
@ -69,7 +64,6 @@ func TestRenderers(t *testing.T) {
change: Change{ change: Change{
renderer: Primitive(strptr("0"), strptr("1")), renderer: Primitive(strptr("0"), strptr("1")),
action: plans.Update, action: plans.Update,
replace: false,
}, },
expected: "0 -> 1", expected: "0 -> 1",
}, },
@ -85,7 +79,6 @@ func TestRenderers(t *testing.T) {
change: Change{ change: Change{
renderer: Sensitive("0", "1", true, true), renderer: Sensitive("0", "1", true, true),
action: plans.Update, action: plans.Update,
replace: false,
}, },
expected: "(sensitive)", expected: "(sensitive)",
}, },
@ -97,6 +90,23 @@ func TestRenderers(t *testing.T) {
}, },
expected: "(sensitive) # forces replacement", expected: "(sensitive) # forces replacement",
}, },
"computed_create": {
change: Change{
renderer: Computed(Change{}),
action: plans.Create,
},
expected: "(known after apply)",
},
"computed_update": {
change: Change{
renderer: Computed(Change{
renderer: Primitive(strptr("0"), nil),
action: plans.Delete,
}),
action: plans.Update,
},
expected: "0 -> (known after apply)",
},
} }
for name, tc := range tcs { for name, tc := range tcs {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
) )
func Sensitive(before, after interface{}, beforeSensitive, afterSensitive bool) Renderer { func Sensitive(before, after interface{}, beforeSensitive, afterSensitive bool) Renderer {
return sensitiveRenderer{ return &sensitiveRenderer{
before: before, before: before,
after: after, after: after,
beforeSensitive: beforeSensitive, beforeSensitive: beforeSensitive,

View File

@ -20,7 +20,7 @@ func ValidateChange(t *testing.T, f ValidateChangeFunc, change Change, expectedA
func ValidatePrimitive(before, after *string) ValidateChangeFunc { func ValidatePrimitive(before, after *string) ValidateChangeFunc {
return func(t *testing.T, change Change) { return func(t *testing.T, change Change) {
primitive, ok := change.renderer.(primitiveRenderer) primitive, ok := change.renderer.(*primitiveRenderer)
if !ok { if !ok {
t.Fatalf("invalid renderer type: %T", change.renderer) t.Fatalf("invalid renderer type: %T", change.renderer)
} }
@ -36,7 +36,7 @@ func ValidatePrimitive(before, after *string) ValidateChangeFunc {
func ValidateSensitive(before, after interface{}, beforeSensitive, afterSensitive bool) ValidateChangeFunc { func ValidateSensitive(before, after interface{}, beforeSensitive, afterSensitive bool) ValidateChangeFunc {
return func(t *testing.T, change Change) { return func(t *testing.T, change Change) {
sensitive, ok := change.renderer.(sensitiveRenderer) sensitive, ok := change.renderer.(*sensitiveRenderer)
if !ok { if !ok {
t.Fatalf("invalid renderer type: %T", change.renderer) t.Fatalf("invalid renderer type: %T", change.renderer)
} }
@ -53,3 +53,25 @@ func ValidateSensitive(before, after interface{}, beforeSensitive, afterSensitiv
} }
} }
} }
func ValidateComputed(before ValidateChangeFunc) ValidateChangeFunc {
return func(t *testing.T, change Change) {
computed, ok := change.renderer.(*computedRenderer)
if !ok {
t.Fatalf("invalid renderer type: %T", change.renderer)
}
if before == nil {
if computed.before.renderer != nil {
t.Fatalf("did not expect a before renderer, but found one")
}
return
}
if computed.before.renderer == nil {
t.Fatalf("expected a before renderer, but found none")
}
before(t, computed.before)
}
}

View File

@ -14,10 +14,14 @@ func (v Value) ComputeChangeForAttribute(attribute *jsonprovider.Attribute) chan
func (v Value) ComputeChangeForType(ctyType cty.Type) change.Change { func (v Value) ComputeChangeForType(ctyType cty.Type) change.Change {
if sensitive, ok := v.CheckForSensitive(); ok { if sensitive, ok := v.checkForSensitive(); ok {
return sensitive return sensitive
} }
if computed, ok := v.checkForComputed(ctyType); ok {
return computed
}
switch { switch {
case ctyType.IsPrimitiveType(): case ctyType.IsPrimitiveType():
return v.computeAttributeChangeAsPrimitive(ctyType) return v.computeAttributeChangeAsPrimitive(ctyType)

View File

@ -0,0 +1,41 @@
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) {
unknown := v.isUnknown()
if !unknown {
return change.Change{}, false
}
// No matter what we do here, we want to treat the after value as explicit.
// This is because it is going to be null in the value, and we don't want
// the functions in this package to assume this means it has been deleted.
v.AfterExplicit = true
if v.Before == nil {
return v.AsChange(change.Computed(change.Change{})), true
}
// If we get here, then we have a before value. We're going to model a
// delete operation and our renderer later can render the overall change
// accurately.
beforeValue := Value{
Before: v.Before,
BeforeSensitive: v.BeforeSensitive,
}
return v.AsChange(change.Computed(beforeValue.ComputeChangeForType(ctyType))), true
}
func (v Value) isUnknown() bool {
if unknown, ok := v.Unknown.(bool); ok {
return unknown
}
return false
}

View File

@ -2,7 +2,7 @@ package differ
import "github.com/hashicorp/terraform/internal/command/jsonformat/change" import "github.com/hashicorp/terraform/internal/command/jsonformat/change"
func (v Value) CheckForSensitive() (change.Change, bool) { func (v Value) checkForSensitive() (change.Change, bool) {
beforeSensitive := v.isBeforeSensitive() beforeSensitive := v.isBeforeSensitive()
afterSensitive := v.isAfterSensitive() afterSensitive := v.isAfterSensitive()

View File

@ -23,9 +23,8 @@ func TestValue_Attribute(t *testing.T) {
attribute: &jsonprovider.Attribute{ attribute: &jsonprovider.Attribute{
AttributeType: []byte("\"string\""), AttributeType: []byte("\"string\""),
}, },
expectedAction: plans.Create, expectedAction: plans.Create,
expectedReplace: false, validateChange: change.ValidatePrimitive(nil, strptr("\"new\"")),
validateChange: change.ValidatePrimitive(nil, strptr("\"new\"")),
}, },
"primitive_delete": { "primitive_delete": {
input: Value{ input: Value{
@ -34,9 +33,8 @@ func TestValue_Attribute(t *testing.T) {
attribute: &jsonprovider.Attribute{ attribute: &jsonprovider.Attribute{
AttributeType: []byte("\"string\""), AttributeType: []byte("\"string\""),
}, },
expectedAction: plans.Delete, expectedAction: plans.Delete,
expectedReplace: false, validateChange: change.ValidatePrimitive(strptr("\"old\""), nil),
validateChange: change.ValidatePrimitive(strptr("\"old\""), nil),
}, },
"primitive_update": { "primitive_update": {
input: Value{ input: Value{
@ -46,9 +44,8 @@ func TestValue_Attribute(t *testing.T) {
attribute: &jsonprovider.Attribute{ attribute: &jsonprovider.Attribute{
AttributeType: []byte("\"string\""), AttributeType: []byte("\"string\""),
}, },
expectedAction: plans.Update, expectedAction: plans.Update,
expectedReplace: false, validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\"")),
validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\"")),
}, },
"primitive_set_explicit_null": { "primitive_set_explicit_null": {
input: Value{ input: Value{
@ -59,9 +56,8 @@ func TestValue_Attribute(t *testing.T) {
attribute: &jsonprovider.Attribute{ attribute: &jsonprovider.Attribute{
AttributeType: []byte("\"string\""), AttributeType: []byte("\"string\""),
}, },
expectedAction: plans.Update, expectedAction: plans.Update,
expectedReplace: false, validateChange: change.ValidatePrimitive(strptr("\"old\""), nil),
validateChange: change.ValidatePrimitive(strptr("\"old\""), nil),
}, },
"primitive_unset_explicit_null": { "primitive_unset_explicit_null": {
input: Value{ input: Value{
@ -72,9 +68,8 @@ func TestValue_Attribute(t *testing.T) {
attribute: &jsonprovider.Attribute{ attribute: &jsonprovider.Attribute{
AttributeType: []byte("\"string\""), AttributeType: []byte("\"string\""),
}, },
expectedAction: plans.Update, expectedAction: plans.Update,
expectedReplace: false, validateChange: change.ValidatePrimitive(nil, strptr("\"new\"")),
validateChange: change.ValidatePrimitive(nil, strptr("\"new\"")),
}, },
"primitive_create_sensitive": { "primitive_create_sensitive": {
input: Value{ input: Value{
@ -85,9 +80,8 @@ func TestValue_Attribute(t *testing.T) {
attribute: &jsonprovider.Attribute{ attribute: &jsonprovider.Attribute{
AttributeType: []byte("\"string\""), AttributeType: []byte("\"string\""),
}, },
expectedAction: plans.Create, expectedAction: plans.Create,
expectedReplace: false, validateChange: change.ValidateSensitive(nil, "new", false, true),
validateChange: change.ValidateSensitive(nil, "new", false, true),
}, },
"primitive_delete_sensitive": { "primitive_delete_sensitive": {
input: Value{ input: Value{
@ -98,9 +92,8 @@ func TestValue_Attribute(t *testing.T) {
attribute: &jsonprovider.Attribute{ attribute: &jsonprovider.Attribute{
AttributeType: []byte("\"string\""), AttributeType: []byte("\"string\""),
}, },
expectedAction: plans.Delete, expectedAction: plans.Delete,
expectedReplace: false, validateChange: change.ValidateSensitive("old", nil, true, false),
validateChange: change.ValidateSensitive("old", nil, true, false),
}, },
"primitive_update_sensitive": { "primitive_update_sensitive": {
input: Value{ input: Value{
@ -112,9 +105,32 @@ func TestValue_Attribute(t *testing.T) {
attribute: &jsonprovider.Attribute{ attribute: &jsonprovider.Attribute{
AttributeType: []byte("\"string\""), AttributeType: []byte("\"string\""),
}, },
expectedAction: plans.Update, expectedAction: plans.Update,
expectedReplace: false, validateChange: change.ValidateSensitive("old", "new", true, true),
validateChange: change.ValidateSensitive("old", "new", true, true), },
"primitive_create_computed": {
input: Value{
Before: nil,
After: nil,
Unknown: true,
},
attribute: &jsonprovider.Attribute{
AttributeType: []byte("\"string\""),
},
expectedAction: plans.Create,
validateChange: change.ValidateComputed(nil),
},
"primitive_update_computed": {
input: Value{
Before: "old",
After: nil,
Unknown: true,
},
attribute: &jsonprovider.Attribute{
AttributeType: []byte("\"string\""),
},
expectedAction: plans.Update,
validateChange: change.ValidateComputed(change.ValidatePrimitive(strptr("\"old\""), nil)),
}, },
} }
for name, tc := range tcs { for name, tc := range tcs {