mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-25 01:00:16 -05: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:
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user