diff --git a/internal/command/jsonformat/change/computed.go b/internal/command/jsonformat/change/computed.go new file mode 100644 index 0000000000..aa8763bec4 --- /dev/null +++ b/internal/command/jsonformat/change/computed.go @@ -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)) +} diff --git a/internal/command/jsonformat/change/primitive.go b/internal/command/jsonformat/change/primitive.go index 464ee55a3c..a84e03f79c 100644 --- a/internal/command/jsonformat/change/primitive.go +++ b/internal/command/jsonformat/change/primitive.go @@ -7,7 +7,7 @@ import ( ) func Primitive(before, after *string) Renderer { - return primitiveRenderer{ + return &primitiveRenderer{ before: before, after: after, } diff --git a/internal/command/jsonformat/change/renderer_test.go b/internal/command/jsonformat/change/renderer_test.go index fcc3102431..62dbd26cef 100644 --- a/internal/command/jsonformat/change/renderer_test.go +++ b/internal/command/jsonformat/change/renderer_test.go @@ -28,7 +28,6 @@ func TestRenderers(t *testing.T) { change: Change{ renderer: Primitive(nil, strptr("1")), action: plans.Create, - replace: false, }, expected: "1", }, @@ -36,7 +35,6 @@ func TestRenderers(t *testing.T) { change: Change{ renderer: Primitive(strptr("1"), nil), action: plans.Delete, - replace: false, }, expected: "1 -> null", }, @@ -44,7 +42,6 @@ func TestRenderers(t *testing.T) { change: Change{ renderer: Primitive(strptr("1"), nil), action: plans.Delete, - replace: false, }, opts: RenderOpts{overrideNullSuffix: true}, expected: "1", @@ -53,7 +50,6 @@ func TestRenderers(t *testing.T) { change: Change{ renderer: Primitive(strptr("1"), nil), action: plans.Update, - replace: false, }, expected: "1 -> null", }, @@ -61,7 +57,6 @@ func TestRenderers(t *testing.T) { change: Change{ renderer: Primitive(nil, strptr("1")), action: plans.Update, - replace: false, }, expected: "null -> 1", }, @@ -69,7 +64,6 @@ func TestRenderers(t *testing.T) { change: Change{ renderer: Primitive(strptr("0"), strptr("1")), action: plans.Update, - replace: false, }, expected: "0 -> 1", }, @@ -85,7 +79,6 @@ func TestRenderers(t *testing.T) { change: Change{ renderer: Sensitive("0", "1", true, true), action: plans.Update, - replace: false, }, expected: "(sensitive)", }, @@ -97,6 +90,23 @@ func TestRenderers(t *testing.T) { }, 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 { t.Run(name, func(t *testing.T) { diff --git a/internal/command/jsonformat/change/sensitive.go b/internal/command/jsonformat/change/sensitive.go index a27de83895..e48b168ab7 100644 --- a/internal/command/jsonformat/change/sensitive.go +++ b/internal/command/jsonformat/change/sensitive.go @@ -6,7 +6,7 @@ import ( ) func Sensitive(before, after interface{}, beforeSensitive, afterSensitive bool) Renderer { - return sensitiveRenderer{ + return &sensitiveRenderer{ before: before, after: after, beforeSensitive: beforeSensitive, diff --git a/internal/command/jsonformat/change/testing.go b/internal/command/jsonformat/change/testing.go index 404dc385c2..4f87843880 100644 --- a/internal/command/jsonformat/change/testing.go +++ b/internal/command/jsonformat/change/testing.go @@ -20,7 +20,7 @@ func ValidateChange(t *testing.T, f ValidateChangeFunc, change Change, expectedA func ValidatePrimitive(before, after *string) ValidateChangeFunc { return func(t *testing.T, change Change) { - primitive, ok := change.renderer.(primitiveRenderer) + primitive, ok := change.renderer.(*primitiveRenderer) if !ok { 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 { return func(t *testing.T, change Change) { - sensitive, ok := change.renderer.(sensitiveRenderer) + sensitive, ok := change.renderer.(*sensitiveRenderer) if !ok { 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) + } +} diff --git a/internal/command/jsonformat/differ/attribute.go b/internal/command/jsonformat/differ/attribute.go index 76ce58edfe..ba0efcb627 100644 --- a/internal/command/jsonformat/differ/attribute.go +++ b/internal/command/jsonformat/differ/attribute.go @@ -14,10 +14,14 @@ func (v Value) ComputeChangeForAttribute(attribute *jsonprovider.Attribute) chan func (v Value) ComputeChangeForType(ctyType cty.Type) change.Change { - if sensitive, ok := v.CheckForSensitive(); ok { + if sensitive, ok := v.checkForSensitive(); ok { return sensitive } + if computed, ok := v.checkForComputed(ctyType); ok { + return computed + } + switch { case ctyType.IsPrimitiveType(): return v.computeAttributeChangeAsPrimitive(ctyType) diff --git a/internal/command/jsonformat/differ/computed.go b/internal/command/jsonformat/differ/computed.go new file mode 100644 index 0000000000..227860f7c7 --- /dev/null +++ b/internal/command/jsonformat/differ/computed.go @@ -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 +} diff --git a/internal/command/jsonformat/differ/sensitive.go b/internal/command/jsonformat/differ/sensitive.go index c286d32e42..98af14c35c 100644 --- a/internal/command/jsonformat/differ/sensitive.go +++ b/internal/command/jsonformat/differ/sensitive.go @@ -2,7 +2,7 @@ package differ 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() afterSensitive := v.isAfterSensitive() diff --git a/internal/command/jsonformat/differ/value_test.go b/internal/command/jsonformat/differ/value_test.go index 9f8140fd87..1c556fd727 100644 --- a/internal/command/jsonformat/differ/value_test.go +++ b/internal/command/jsonformat/differ/value_test.go @@ -23,9 +23,8 @@ func TestValue_Attribute(t *testing.T) { attribute: &jsonprovider.Attribute{ AttributeType: []byte("\"string\""), }, - expectedAction: plans.Create, - expectedReplace: false, - validateChange: change.ValidatePrimitive(nil, strptr("\"new\"")), + expectedAction: plans.Create, + validateChange: change.ValidatePrimitive(nil, strptr("\"new\"")), }, "primitive_delete": { input: Value{ @@ -34,9 +33,8 @@ func TestValue_Attribute(t *testing.T) { attribute: &jsonprovider.Attribute{ AttributeType: []byte("\"string\""), }, - expectedAction: plans.Delete, - expectedReplace: false, - validateChange: change.ValidatePrimitive(strptr("\"old\""), nil), + expectedAction: plans.Delete, + validateChange: change.ValidatePrimitive(strptr("\"old\""), nil), }, "primitive_update": { input: Value{ @@ -46,9 +44,8 @@ func TestValue_Attribute(t *testing.T) { attribute: &jsonprovider.Attribute{ AttributeType: []byte("\"string\""), }, - expectedAction: plans.Update, - expectedReplace: false, - validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\"")), + expectedAction: plans.Update, + validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\"")), }, "primitive_set_explicit_null": { input: Value{ @@ -59,9 +56,8 @@ func TestValue_Attribute(t *testing.T) { attribute: &jsonprovider.Attribute{ AttributeType: []byte("\"string\""), }, - expectedAction: plans.Update, - expectedReplace: false, - validateChange: change.ValidatePrimitive(strptr("\"old\""), nil), + expectedAction: plans.Update, + validateChange: change.ValidatePrimitive(strptr("\"old\""), nil), }, "primitive_unset_explicit_null": { input: Value{ @@ -72,9 +68,8 @@ func TestValue_Attribute(t *testing.T) { attribute: &jsonprovider.Attribute{ AttributeType: []byte("\"string\""), }, - expectedAction: plans.Update, - expectedReplace: false, - validateChange: change.ValidatePrimitive(nil, strptr("\"new\"")), + expectedAction: plans.Update, + validateChange: change.ValidatePrimitive(nil, strptr("\"new\"")), }, "primitive_create_sensitive": { input: Value{ @@ -85,9 +80,8 @@ func TestValue_Attribute(t *testing.T) { attribute: &jsonprovider.Attribute{ AttributeType: []byte("\"string\""), }, - expectedAction: plans.Create, - expectedReplace: false, - validateChange: change.ValidateSensitive(nil, "new", false, true), + expectedAction: plans.Create, + validateChange: change.ValidateSensitive(nil, "new", false, true), }, "primitive_delete_sensitive": { input: Value{ @@ -98,9 +92,8 @@ func TestValue_Attribute(t *testing.T) { attribute: &jsonprovider.Attribute{ AttributeType: []byte("\"string\""), }, - expectedAction: plans.Delete, - expectedReplace: false, - validateChange: change.ValidateSensitive("old", nil, true, false), + expectedAction: plans.Delete, + validateChange: change.ValidateSensitive("old", nil, true, false), }, "primitive_update_sensitive": { input: Value{ @@ -112,9 +105,32 @@ func TestValue_Attribute(t *testing.T) { attribute: &jsonprovider.Attribute{ AttributeType: []byte("\"string\""), }, - expectedAction: plans.Update, - expectedReplace: false, - validateChange: change.ValidateSensitive("old", "new", true, true), + expectedAction: plans.Update, + 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 {