Add support for outputs in the structured renderer (#32426)

* 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

* Add support for the replace paths data in the structured renderer

* Add support for maps in the structured renderer

* Add support for lists in the structured renderer

* goimports

* Add support for sets in the structured renderer

* goimports

* Add support for blocks in the structured renderer

* goimports

* Add support for outputs in the structured renderer

* fix ordering of blocks

* remove unused test stub

* fix typo
This commit is contained in:
Liam Cervante 2023-01-09 14:45:35 +01:00 committed by GitHub
parent 05f1764a0d
commit 9bc5ded27a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 384 additions and 2 deletions

View File

@ -1471,6 +1471,44 @@ func TestRenderers(t *testing.T) {
# (2 unchanged blocks hidden)
}`,
},
"output_map_to_list": {
change: Change{
renderer: TypeChange(Change{
renderer: Map(map[string]Change{
"element_one": {
renderer: Primitive(strptr("0"), nil),
action: plans.Delete,
},
"element_two": {
renderer: Primitive(strptr("1"), nil),
action: plans.Delete,
},
}),
action: plans.Delete,
}, Change{
renderer: List([]Change{
{
renderer: Primitive(nil, strptr("0")),
action: plans.Create,
},
{
renderer: Primitive(nil, strptr("1")),
action: plans.Create,
},
}),
action: plans.Create,
}),
},
expected: `
{
- "element_one" = 0
- "element_two" = 1
} -> [
+ 0,
+ 1,
]
`,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {

View File

@ -243,6 +243,20 @@ func ValidateBlock(attributes map[string]ValidateChangeFunc, blocks map[string][
}
}
func ValidateTypeChange(before, after ValidateChangeFunc, action plans.Action, replace bool) ValidateChangeFunc {
return func(t *testing.T, change Change) {
validateChange(t, change, action, replace)
typeChange, ok := change.renderer.(*typeChangeRenderer)
if !ok {
t.Fatalf("invalid renderer type: %T", change.renderer)
}
before(t, typeChange.before)
after(t, typeChange.after)
}
}
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)

View File

@ -0,0 +1,22 @@
package change
import "fmt"
func TypeChange(before, after Change) Renderer {
return &typeChangeRenderer{
before: before,
after: after,
}
}
type typeChangeRenderer struct {
NoWarningsRenderer
before Change
after Change
}
func (renderer typeChangeRenderer) Render(change Change, indent int, opts RenderOpts) string {
opts.overrideNullSuffix = true // Never render null suffix for children of type changes.
return fmt.Sprintf("%s [yellow]->[reset] %s", renderer.before.Render(indent, opts), renderer.after.Render(indent, opts))
}

View File

@ -31,6 +31,10 @@ func (v Value) computeChangeForNestedAttribute(attribute *jsonprovider.NestedTyp
}
func (v Value) computeChangeForType(ctyType cty.Type) change.Change {
if ctyType == cty.NilType {
return v.ComputeChangeForOutput()
}
switch {
case ctyType.IsPrimitiveType():
return v.computeAttributeChangeAsPrimitive(ctyType)

View File

@ -1,7 +1,86 @@
package differ
import "github.com/hashicorp/terraform/internal/command/jsonformat/change"
import (
"fmt"
"github.com/hashicorp/terraform/internal/command/jsonformat/change"
"github.com/hashicorp/terraform/internal/plans"
"github.com/zclconf/go-cty/cty"
)
const (
jsonNumber = "number"
jsonObject = "object"
jsonArray = "array"
jsonBool = "bool"
jsonString = "string"
jsonNull = "null"
)
func (v Value) ComputeChangeForOutput() change.Change {
panic("not implemented")
beforeType := getJsonType(v.Before)
afterType := getJsonType(v.After)
valueToAttribute := func(v Value, jsonType string) change.Change {
var res change.Change
switch jsonType {
case jsonNull:
res = v.computeAttributeChangeAsPrimitive(cty.NilType)
case jsonBool:
res = v.computeAttributeChangeAsPrimitive(cty.Bool)
case jsonString:
res = v.computeAttributeChangeAsPrimitive(cty.String)
case jsonNumber:
res = v.computeAttributeChangeAsPrimitive(cty.Number)
case jsonObject:
res = v.computeAttributeChangeAsMap(cty.NilType)
case jsonArray:
res = v.computeAttributeChangeAsList(cty.NilType)
default:
panic("unrecognized json type: " + jsonType)
}
return res
}
if beforeType == afterType || (beforeType == jsonNull || afterType == jsonNull) {
targetType := beforeType
if targetType == jsonNull {
targetType = afterType
}
return valueToAttribute(v, targetType)
}
before := valueToAttribute(Value{
Before: v.Before,
BeforeSensitive: v.BeforeSensitive,
}, beforeType)
after := valueToAttribute(Value{
After: v.After,
AfterSensitive: v.AfterSensitive,
Unknown: v.Unknown,
}, afterType)
return change.New(change.TypeChange(before, after), plans.Update, false)
}
func getJsonType(json interface{}) string {
switch json.(type) {
case []interface{}:
return jsonArray
case float64:
return jsonNumber
case string:
return jsonString
case bool:
return jsonBool
case nil:
return jsonNull
case map[string]interface{}:
return jsonObject
default:
panic(fmt.Sprintf("unrecognized json type %T", json))
}
}

View File

@ -1208,6 +1208,231 @@ func TestValue_BlockAttributesAndNestedBlocks(t *testing.T) {
}
}
func TestValue_Outputs(t *testing.T) {
tcs := map[string]struct {
input Value
validateChange change.ValidateChangeFunc
}{
"primitive_create": {
input: Value{
Before: nil,
After: "new",
},
validateChange: change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
},
"map_create": {
input: Value{
Before: nil,
After: map[string]interface{}{
"element_one": "new_one",
"element_two": "new_two",
},
},
validateChange: change.ValidateMap(map[string]change.ValidateChangeFunc{
"element_one": change.ValidatePrimitive(nil, strptr("\"new_one\""), plans.Create, false),
"element_two": change.ValidatePrimitive(nil, strptr("\"new_two\""), plans.Create, false),
}, plans.Create, false),
},
"list_create": {
input: Value{
Before: nil,
After: []interface{}{
"new_one",
"new_two",
},
},
validateChange: change.ValidateList([]change.ValidateChangeFunc{
change.ValidatePrimitive(nil, strptr("\"new_one\""), plans.Create, false),
change.ValidatePrimitive(nil, strptr("\"new_two\""), plans.Create, false),
}, plans.Create, false),
},
"primitive_update": {
input: Value{
Before: "old",
After: "new",
},
validateChange: change.ValidatePrimitive(strptr("\"old\""), strptr("\"new\""), plans.Update, false),
},
"map_update": {
input: Value{
Before: map[string]interface{}{
"element_one": "old_one",
"element_two": "old_two",
},
After: map[string]interface{}{
"element_one": "new_one",
"element_two": "new_two",
},
},
validateChange: change.ValidateMap(map[string]change.ValidateChangeFunc{
"element_one": change.ValidatePrimitive(strptr("\"old_one\""), strptr("\"new_one\""), plans.Update, false),
"element_two": change.ValidatePrimitive(strptr("\"old_two\""), strptr("\"new_two\""), plans.Update, false),
}, plans.Update, false),
},
"list_update": {
input: Value{
Before: []interface{}{
"old_one",
"old_two",
},
After: []interface{}{
"new_one",
"new_two",
},
},
validateChange: change.ValidateList([]change.ValidateChangeFunc{
change.ValidatePrimitive(strptr("\"old_one\""), nil, plans.Delete, false),
change.ValidatePrimitive(strptr("\"old_two\""), nil, plans.Delete, false),
change.ValidatePrimitive(nil, strptr("\"new_one\""), plans.Create, false),
change.ValidatePrimitive(nil, strptr("\"new_two\""), plans.Create, false),
}, plans.Update, false),
},
"primitive_delete": {
input: Value{
Before: "old",
After: nil,
},
validateChange: change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
},
"map_delete": {
input: Value{
Before: map[string]interface{}{
"element_one": "old_one",
"element_two": "old_two",
},
After: nil,
},
validateChange: change.ValidateMap(map[string]change.ValidateChangeFunc{
"element_one": change.ValidatePrimitive(strptr("\"old_one\""), nil, plans.Delete, false),
"element_two": change.ValidatePrimitive(strptr("\"old_two\""), nil, plans.Delete, false),
}, plans.Delete, false),
},
"list_delete": {
input: Value{
Before: []interface{}{
"old_one",
"old_two",
},
After: nil,
},
validateChange: change.ValidateList([]change.ValidateChangeFunc{
change.ValidatePrimitive(strptr("\"old_one\""), nil, plans.Delete, false),
change.ValidatePrimitive(strptr("\"old_two\""), nil, plans.Delete, false),
}, plans.Delete, false),
},
"primitive_to_list": {
input: Value{
Before: "old",
After: []interface{}{
"new_one",
"new_two",
},
},
validateChange: change.ValidateTypeChange(
change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
change.ValidateList([]change.ValidateChangeFunc{
change.ValidatePrimitive(nil, strptr("\"new_one\""), plans.Create, false),
change.ValidatePrimitive(nil, strptr("\"new_two\""), plans.Create, false),
}, plans.Create, false), plans.Update, false),
},
"primitive_to_map": {
input: Value{
Before: "old",
After: map[string]interface{}{
"element_one": "new_one",
"element_two": "new_two",
},
},
validateChange: change.ValidateTypeChange(
change.ValidatePrimitive(strptr("\"old\""), nil, plans.Delete, false),
change.ValidateMap(map[string]change.ValidateChangeFunc{
"element_one": change.ValidatePrimitive(nil, strptr("\"new_one\""), plans.Create, false),
"element_two": change.ValidatePrimitive(nil, strptr("\"new_two\""), plans.Create, false),
}, plans.Create, false), plans.Update, false),
},
"list_to_primitive": {
input: Value{
Before: []interface{}{
"old_one",
"old_two",
},
After: "new",
},
validateChange: change.ValidateTypeChange(
change.ValidateList([]change.ValidateChangeFunc{
change.ValidatePrimitive(strptr("\"old_one\""), nil, plans.Delete, false),
change.ValidatePrimitive(strptr("\"old_two\""), nil, plans.Delete, false),
}, plans.Delete, false),
change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
plans.Update, false),
},
"list_to_map": {
input: Value{
Before: []interface{}{
"old_one",
"old_two",
},
After: map[string]interface{}{
"element_one": "new_one",
"element_two": "new_two",
},
},
validateChange: change.ValidateTypeChange(
change.ValidateList([]change.ValidateChangeFunc{
change.ValidatePrimitive(strptr("\"old_one\""), nil, plans.Delete, false),
change.ValidatePrimitive(strptr("\"old_two\""), nil, plans.Delete, false),
}, plans.Delete, false),
change.ValidateMap(map[string]change.ValidateChangeFunc{
"element_one": change.ValidatePrimitive(nil, strptr("\"new_one\""), plans.Create, false),
"element_two": change.ValidatePrimitive(nil, strptr("\"new_two\""), plans.Create, false),
}, plans.Create, false), plans.Update, false),
},
"map_to_primitive": {
input: Value{
Before: map[string]interface{}{
"element_one": "old_one",
"element_two": "old_two",
},
After: "new",
},
validateChange: change.ValidateTypeChange(
change.ValidateMap(map[string]change.ValidateChangeFunc{
"element_one": change.ValidatePrimitive(strptr("\"old_one\""), nil, plans.Delete, false),
"element_two": change.ValidatePrimitive(strptr("\"old_two\""), nil, plans.Delete, false),
}, plans.Delete, false),
change.ValidatePrimitive(nil, strptr("\"new\""), plans.Create, false),
plans.Update, false),
},
"map_to_list": {
input: Value{
Before: map[string]interface{}{
"element_one": "old_one",
"element_two": "old_two",
},
After: []interface{}{
"new_one",
"new_two",
},
},
validateChange: change.ValidateTypeChange(
change.ValidateMap(map[string]change.ValidateChangeFunc{
"element_one": change.ValidatePrimitive(strptr("\"old_one\""), nil, plans.Delete, false),
"element_two": change.ValidatePrimitive(strptr("\"old_two\""), nil, plans.Delete, false),
}, plans.Delete, false),
change.ValidateList([]change.ValidateChangeFunc{
change.ValidatePrimitive(nil, strptr("\"new_one\""), plans.Create, false),
change.ValidatePrimitive(nil, strptr("\"new_two\""), plans.Create, false),
}, plans.Create, false), plans.Update, false),
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
tc.validateChange(t, tc.input.ComputeChange(cty.NilType))
})
}
}
func TestValue_PrimitiveAttributes(t *testing.T) {
// This function tests manipulating primitives: creating, deleting and
// updating. It also automatically tests these operations within the