mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Add rendering functionality for primitives to the structured renderer (#32373)
* 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 * goimports
This commit is contained in:
parent
aff7d360e1
commit
71daef058f
@ -55,3 +55,21 @@ func (change Change) Render(indent int, opts RenderOpts) string {
|
||||
func (change Change) Warnings(indent int) []string {
|
||||
return change.renderer.Warnings(change, indent)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if !override && change.action == plans.Delete {
|
||||
return " [dark_gray]-> null[reset]"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// forcesReplacement returns the `# forces replacement` suffix if this change is
|
||||
// driving the entire resource to be replaced.
|
||||
func (change Change) forcesReplacement() string {
|
||||
if change.replace {
|
||||
return " [red]# forces replacement[reset]"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
48
internal/command/jsonformat/change/primitive.go
Normal file
48
internal/command/jsonformat/change/primitive.go
Normal file
@ -0,0 +1,48 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
func Primitive(before, after *string) Renderer {
|
||||
return primitiveRenderer{
|
||||
before: before,
|
||||
after: after,
|
||||
}
|
||||
}
|
||||
|
||||
type primitiveRenderer struct {
|
||||
NoWarningsRenderer
|
||||
|
||||
before *string
|
||||
after *string
|
||||
}
|
||||
|
||||
func (render primitiveRenderer) Render(result Change, indent int, opts RenderOpts) string {
|
||||
var beforeValue, afterValue string
|
||||
|
||||
if render.before != nil {
|
||||
beforeValue = *render.before
|
||||
} else {
|
||||
beforeValue = "[dark_gray]null[reset]"
|
||||
}
|
||||
|
||||
if render.after != nil {
|
||||
afterValue = *render.after
|
||||
} else {
|
||||
afterValue = "[dark_gray]null[reset]"
|
||||
}
|
||||
|
||||
switch result.action {
|
||||
case plans.Create:
|
||||
return fmt.Sprintf("%s%s", afterValue, result.forcesReplacement())
|
||||
case plans.Delete:
|
||||
return fmt.Sprintf("%s%s%s", beforeValue, result.nullSuffix(opts.overrideNullSuffix), result.forcesReplacement())
|
||||
case plans.NoOp:
|
||||
return fmt.Sprintf("%s%s", beforeValue, result.forcesReplacement())
|
||||
default:
|
||||
return fmt.Sprintf("%s [yellow]->[reset] %s%s", beforeValue, afterValue, result.forcesReplacement())
|
||||
}
|
||||
}
|
@ -22,13 +22,22 @@ func (render NoWarningsRenderer) Warnings(change Change, indent int) []string {
|
||||
|
||||
// RenderOpts contains options that can control how the Renderer.Render function
|
||||
// will render.
|
||||
//
|
||||
// For now, we haven't implemented any of the Renderer functionality, so we have
|
||||
// no options currently.
|
||||
type RenderOpts struct{}
|
||||
type RenderOpts struct {
|
||||
|
||||
// overrideNullSuffix tells the Renderer not to display the `-> null` suffix
|
||||
// that is normally displayed when an element, attribute, or block is
|
||||
// deleted.
|
||||
//
|
||||
// The presence of this suffix is decided by the parent changes of a given
|
||||
// change, as such we provide this as an option instead of trying to
|
||||
// calculate it inside a specific renderer.
|
||||
overrideNullSuffix bool
|
||||
}
|
||||
|
||||
// Clone returns a new RenderOpts object, that matches the original but can be
|
||||
// edited without changing the original.
|
||||
func (opts RenderOpts) Clone() RenderOpts {
|
||||
return RenderOpts{}
|
||||
return RenderOpts{
|
||||
overrideNullSuffix: opts.overrideNullSuffix,
|
||||
}
|
||||
}
|
||||
|
86
internal/command/jsonformat/change/renderer_test.go
Normal file
86
internal/command/jsonformat/change/renderer_test.go
Normal file
@ -0,0 +1,86 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/mitchellh/colorstring"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
func TestRenderers(t *testing.T) {
|
||||
strptr := func(in string) *string {
|
||||
return &in
|
||||
}
|
||||
|
||||
colorize := colorstring.Colorize{
|
||||
Colors: colorstring.DefaultColors,
|
||||
Disable: true,
|
||||
}
|
||||
|
||||
tcs := map[string]struct {
|
||||
change Change
|
||||
expected string
|
||||
opts RenderOpts
|
||||
}{
|
||||
"primitive_create": {
|
||||
change: Change{
|
||||
renderer: Primitive(nil, strptr("1")),
|
||||
action: plans.Create,
|
||||
replace: false,
|
||||
},
|
||||
expected: "1",
|
||||
},
|
||||
"primitive_delete": {
|
||||
change: Change{
|
||||
renderer: Primitive(strptr("1"), nil),
|
||||
action: plans.Delete,
|
||||
replace: false,
|
||||
},
|
||||
expected: "1 -> null",
|
||||
},
|
||||
"primitive_delete_override": {
|
||||
change: Change{
|
||||
renderer: Primitive(strptr("1"), nil),
|
||||
action: plans.Delete,
|
||||
replace: false,
|
||||
},
|
||||
opts: RenderOpts{overrideNullSuffix: true},
|
||||
expected: "1",
|
||||
},
|
||||
"primitive_update_to_null": {
|
||||
change: Change{
|
||||
renderer: Primitive(strptr("1"), nil),
|
||||
action: plans.Update,
|
||||
replace: false,
|
||||
},
|
||||
expected: "1 -> null",
|
||||
},
|
||||
"primitive_update_from_null": {
|
||||
change: Change{
|
||||
renderer: Primitive(nil, strptr("1")),
|
||||
action: plans.Update,
|
||||
replace: false,
|
||||
},
|
||||
expected: "null -> 1",
|
||||
},
|
||||
"primitive_update": {
|
||||
change: Change{
|
||||
renderer: Primitive(strptr("0"), strptr("1")),
|
||||
action: plans.Update,
|
||||
replace: false,
|
||||
},
|
||||
expected: "0 -> 1",
|
||||
},
|
||||
}
|
||||
for name, tc := range tcs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
35
internal/command/jsonformat/change/testing.go
Normal file
35
internal/command/jsonformat/change/testing.go
Normal file
@ -0,0 +1,35 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
type ValidateChangeFunc func(t *testing.T, change Change)
|
||||
|
||||
func ValidateChange(t *testing.T, f ValidateChangeFunc, 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 {
|
||||
return func(t *testing.T, change Change) {
|
||||
primitive, ok := change.renderer.(primitiveRenderer)
|
||||
if !ok {
|
||||
t.Fatalf("invalid renderer type: %T", change.renderer)
|
||||
}
|
||||
|
||||
beforeDiff := cmp.Diff(primitive.before, before)
|
||||
afterDiff := cmp.Diff(primitive.after, after)
|
||||
|
||||
if len(beforeDiff) > 0 || len(afterDiff) > 0 {
|
||||
t.Fatalf("before diff: (%s), after diff: (%s)", beforeDiff, afterDiff)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,38 @@
|
||||
package differ
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
func (v Value) ComputeChangeForAttribute(attribute *jsonprovider.Attribute) change.Change {
|
||||
panic("not implemented")
|
||||
return v.ComputeChangeForType(unmarshalAttribute(attribute))
|
||||
}
|
||||
|
||||
func (v Value) ComputeChangeForType(ctyType cty.Type) change.Change {
|
||||
switch {
|
||||
case ctyType.IsPrimitiveType():
|
||||
return v.computeAttributeChangeAsPrimitive(ctyType)
|
||||
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())
|
||||
}
|
||||
return ctyType
|
||||
}
|
||||
|
37
internal/command/jsonformat/differ/primitive.go
Normal file
37
internal/command/jsonformat/differ/primitive.go
Normal file
@ -0,0 +1,37 @@
|
||||
package differ
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/change"
|
||||
)
|
||||
|
||||
func strptr(str string) *string {
|
||||
return &str
|
||||
}
|
||||
|
||||
func (v Value) computeAttributeChangeAsPrimitive(ctyType cty.Type) change.Change {
|
||||
return v.AsChange(change.Primitive(formatAsPrimitive(v.Before, ctyType), formatAsPrimitive(v.After, ctyType)))
|
||||
}
|
||||
|
||||
func formatAsPrimitive(value interface{}, ctyType cty.Type) *string {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case ctyType == cty.String:
|
||||
return strptr(fmt.Sprintf("\"%s\"", value))
|
||||
case ctyType == cty.Bool:
|
||||
if value.(bool) {
|
||||
return strptr("true")
|
||||
}
|
||||
return strptr("false")
|
||||
case ctyType == cty.Number:
|
||||
return strptr(fmt.Sprintf("%g", value))
|
||||
default:
|
||||
panic("unrecognized primitive type: " + ctyType.FriendlyName())
|
||||
}
|
||||
}
|
@ -2,8 +2,15 @@ package differ
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/change"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonplan"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
// Value contains the unmarshalled generic interface{} types that are output by
|
||||
@ -26,19 +33,20 @@ import (
|
||||
// the type would need to change between the before and after value. It is in
|
||||
// fact just easier to iterate through the values as generic JSON interfaces.
|
||||
type Value struct {
|
||||
// BeforeExplicit refers to whether the Before value is explicit or
|
||||
|
||||
// BeforeExplicit matches AfterExplicit except references the Before value.
|
||||
BeforeExplicit bool
|
||||
|
||||
// AfterExplicit refers to whether the After value is explicit or
|
||||
// implicit. It is explicit if it has been specified by the user, and
|
||||
// implicit if it has been set as a consequence of other changes.
|
||||
//
|
||||
// For example, explicitly setting a value to null in a list should result
|
||||
// in Before being null and BeforeExplicit being true. In comparison,
|
||||
// removing an element from a list should also result in Before being null
|
||||
// and BeforeExplicit being false. Without the explicit information our
|
||||
// in After being null and AfterExplicit being true. In comparison,
|
||||
// removing an element from a list should also result in After being null
|
||||
// and AfterExplicit being false. Without the explicit information our
|
||||
// functions would not be able to tell the difference between these two
|
||||
// cases.
|
||||
BeforeExplicit bool
|
||||
|
||||
// AfterExplicit matches BeforeExplicit except references the After value.
|
||||
AfterExplicit bool
|
||||
|
||||
// Before contains the value before the proposed change.
|
||||
@ -90,6 +98,62 @@ func ValueFromJsonChange(change jsonplan.Change) Value {
|
||||
}
|
||||
}
|
||||
|
||||
// ComputeChange is a generic function that lets callers no worry about what
|
||||
// type of change they are processing.
|
||||
//
|
||||
// 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 {
|
||||
switch concrete := changeType.(type) {
|
||||
case *jsonprovider.Attribute:
|
||||
return v.ComputeChangeForAttribute(concrete)
|
||||
case cty.Type:
|
||||
return v.ComputeChangeForType(concrete)
|
||||
default:
|
||||
panic(fmt.Sprintf("unrecognized change type: %T", changeType))
|
||||
}
|
||||
}
|
||||
|
||||
func (v Value) AsChange(renderer change.Renderer) change.Change {
|
||||
return change.New(renderer, v.calculateChange(), v.replacePath())
|
||||
}
|
||||
|
||||
func (v Value) isBeforeSensitive() bool {
|
||||
if sensitive, ok := v.BeforeSensitive.(bool); ok {
|
||||
return sensitive
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (v Value) isAfterSensitive() bool {
|
||||
if sensitive, ok := v.AfterSensitive.(bool); ok {
|
||||
return sensitive
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (v Value) replacePath() bool {
|
||||
if replace, ok := v.ReplacePaths.(bool); ok {
|
||||
return replace
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (v Value) calculateChange() plans.Action {
|
||||
if (v.Before == nil && !v.BeforeExplicit) && (v.After != nil || v.AfterExplicit) {
|
||||
return plans.Create
|
||||
}
|
||||
if (v.After == nil && !v.AfterExplicit) && (v.Before != nil || v.BeforeExplicit) {
|
||||
return plans.Delete
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(v.Before, v.After) && v.AfterExplicit == v.BeforeExplicit && v.isAfterSensitive() == v.isBeforeSensitive() {
|
||||
return plans.NoOp
|
||||
}
|
||||
|
||||
return plans.Update
|
||||
}
|
||||
|
||||
func unmarshalGeneric(raw json.RawMessage) interface{} {
|
||||
if raw == nil {
|
||||
return nil
|
||||
|
90
internal/command/jsonformat/differ/value_test.go
Normal file
90
internal/command/jsonformat/differ/value_test.go
Normal file
@ -0,0 +1,90 @@
|
||||
package differ
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/change"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
func TestValue_Attribute(t *testing.T) {
|
||||
tcs := map[string]struct {
|
||||
input Value
|
||||
attribute *jsonprovider.Attribute
|
||||
expectedAction plans.Action
|
||||
expectedReplace bool
|
||||
validateChange change.ValidateChangeFunc
|
||||
}{
|
||||
"primitive_create": {
|
||||
input: Value{
|
||||
After: "new",
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Create,
|
||||
expectedReplace: false,
|
||||
validateChange: change.ValidatePrimitive(nil, strptr("\"new\"")),
|
||||
},
|
||||
"primitive_delete": {
|
||||
input: Value{
|
||||
Before: "old",
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Delete,
|
||||
expectedReplace: false,
|
||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), nil),
|
||||
},
|
||||
"primitive_update": {
|
||||
input: Value{
|
||||
Before: "old",
|
||||
After: "new",
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Update,
|
||||
expectedReplace: false,
|
||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\"")),
|
||||
},
|
||||
"primitive_set_explicit_null": {
|
||||
input: Value{
|
||||
Before: "old",
|
||||
After: nil,
|
||||
AfterExplicit: true,
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Update,
|
||||
expectedReplace: false,
|
||||
validateChange: change.ValidatePrimitive(strptr("\"old\""), nil),
|
||||
},
|
||||
"primitive_unset_explicit_null": {
|
||||
input: Value{
|
||||
BeforeExplicit: true,
|
||||
Before: nil,
|
||||
After: "new",
|
||||
},
|
||||
attribute: &jsonprovider.Attribute{
|
||||
AttributeType: []byte("\"string\""),
|
||||
},
|
||||
expectedAction: plans.Update,
|
||||
expectedReplace: false,
|
||||
validateChange: change.ValidatePrimitive(nil, strptr("\"new\"")),
|
||||
},
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user