Structured plan renderer: Implement the main functionality for the renderer (#32496)

* raw unmodified broken tests

* tests execute, no panics

* fix whitespace differences

* fix all the tests

* fix tests

* actually fix tests

* add missing plan metadata into the renderer

* address comments

* complete merge

* remove TODO raising questions about outputs, they are fixed

* missing bold on plan
This commit is contained in:
Liam Cervante 2023-01-12 17:59:07 +01:00 committed by GitHub
parent af0ff90d6e
commit 95782f2491
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 7858 additions and 255 deletions

View File

@ -68,7 +68,7 @@ type DiffRenderer interface {
// RenderHumanOpts contains options that can control how the human render
// function of the DiffRenderer will function.
type RenderHumanOpts struct {
Colorize colorstring.Colorize
Colorize *colorstring.Colorize
// OverrideNullSuffix tells the Renderer not to display the `-> null` suffix
// that is normally displayed when an element, attribute, or block is
@ -91,7 +91,7 @@ type RenderHumanOpts struct {
// NewRenderHumanOpts creates a new RenderHumanOpts struct with the required
// fields set.
func NewRenderHumanOpts(colorize colorstring.Colorize) RenderHumanOpts {
func NewRenderHumanOpts(colorize *colorstring.Colorize) RenderHumanOpts {
return RenderHumanOpts{
Colorize: colorize,
}

View File

@ -16,6 +16,8 @@ var (
importantAttributes = []string{
"id",
"name",
"tags",
}
)
@ -43,6 +45,10 @@ type blockRenderer struct {
}
func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string {
if len(renderer.attributes) == 0 && len(renderer.blocks.GetAllKeys()) == 0 {
return fmt.Sprintf("{}%s", forcesReplacement(diff.Replace, opts))
}
unchangedAttributes := 0
unchangedBlocks := 0
@ -59,19 +65,24 @@ func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts c
}
sort.Strings(attributeKeys)
importantAttributeOpts := opts.Clone()
importantAttributeOpts.ShowUnchangedChildren = true
attributeOpts := opts.Clone()
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("{%s\n", forcesReplacement(diff.Replace, opts)))
for _, importantKey := range importantAttributes {
if attribute, ok := renderer.attributes[importantKey]; ok {
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", formatIndent(indent+1), colorizeDiffAction(attribute.Action, opts), maximumAttributeKeyLen, importantKey, attribute.RenderHuman(indent+1, opts)))
}
}
for _, key := range attributeKeys {
attribute := renderer.attributes[key]
if importantAttribute(key) {
// Always display the important attributes.
for _, warning := range attribute.WarningsHuman(indent+1, importantAttributeOpts) {
buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning))
}
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", formatIndent(indent+1), colorizeDiffAction(attribute.Action, importantAttributeOpts), maximumAttributeKeyLen, key, attribute.RenderHuman(indent+1, importantAttributeOpts)))
continue
}
attribute := renderer.attributes[key]
if attribute.Action == plans.NoOp && !opts.ShowUnchangedChildren {
unchangedAttributes++
continue
@ -80,7 +91,7 @@ func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts c
for _, warning := range attribute.WarningsHuman(indent+1, opts) {
buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning))
}
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", formatIndent(indent+1), colorizeDiffAction(attribute.Action, opts), maximumAttributeKeyLen, escapedAttributeKeys[key], attribute.RenderHuman(indent+1, opts)))
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", formatIndent(indent+1), colorizeDiffAction(attribute.Action, attributeOpts), maximumAttributeKeyLen, escapedAttributeKeys[key], attribute.RenderHuman(indent+1, attributeOpts)))
}
if unchangedAttributes > 0 {
@ -92,6 +103,22 @@ func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts c
foundChangedBlock := false
renderBlock := func(diff computed.Diff, mapKey string, opts computed.RenderHumanOpts) {
creatingSensitiveValue := diff.Action == plans.Create && renderer.blocks.AfterSensitiveBlocks[key]
deletingSensitiveValue := diff.Action == plans.Delete && renderer.blocks.BeforeSensitiveBlocks[key]
modifyingSensitiveValue := (diff.Action == plans.Update || diff.Action == plans.NoOp) && (renderer.blocks.AfterSensitiveBlocks[key] || renderer.blocks.BeforeSensitiveBlocks[key])
if creatingSensitiveValue || deletingSensitiveValue || modifyingSensitiveValue {
// Intercept the renderer here if the sensitive data was set
// across all the blocks instead of individually.
action := diff.Action
if diff.Action == plans.NoOp && renderer.blocks.BeforeSensitiveBlocks[key] != renderer.blocks.AfterSensitiveBlocks[key] {
action = plans.Update
}
diff = computed.NewDiff(SensitiveBlock(diff, renderer.blocks.BeforeSensitiveBlocks[key], renderer.blocks.AfterSensitiveBlocks[key]), action, diff.Replace)
}
if diff.Action == plans.NoOp && !opts.ShowUnchangedChildren {
unchangedBlocks++
return
@ -104,10 +131,17 @@ func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts c
foundChangedBlock = true
}
for _, warning := range diff.WarningsHuman(indent+1, opts) {
// If the force replacement metadata was set for every entry in the
// block we need to override that here. Our child blocks will only
// know about the replace function if it was set on them
// specifically, and not if it was set for all the blocks.
blockOpts := opts.Clone()
blockOpts.OverrideForcesReplacement = renderer.blocks.ReplaceBlocks[key]
for _, warning := range diff.WarningsHuman(indent+1, blockOpts) {
buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning))
}
buf.WriteString(fmt.Sprintf("%s%s %s%s %s\n", formatIndent(indent+1), colorizeDiffAction(diff.Action, opts), ensureValidAttributeName(key), mapKey, diff.RenderHuman(indent+1, opts)))
buf.WriteString(fmt.Sprintf("%s%s %s%s %s\n", formatIndent(indent+1), colorizeDiffAction(diff.Action, blockOpts), ensureValidAttributeName(key), mapKey, diff.RenderHuman(indent+1, blockOpts)))
}
@ -140,7 +174,7 @@ func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts c
}
if unchangedBlocks > 0 {
buf.WriteString(fmt.Sprintf("%s%s %s\n", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), unchanged("block", unchangedBlocks, opts)))
buf.WriteString(fmt.Sprintf("\n%s%s %s\n", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), unchanged("block", unchangedBlocks, opts)))
}
buf.WriteString(fmt.Sprintf("%s%s }", formatIndent(indent), format.DiffActionSymbol(plans.NoOp)))

View File

@ -13,6 +13,17 @@ type Blocks struct {
ListBlocks map[string][]computed.Diff
SetBlocks map[string][]computed.Diff
MapBlocks map[string]map[string]computed.Diff
// ReplaceBlocks and Before/AfterSensitiveBlocks carry forward the
// information about an entire group of blocks (eg. if all the blocks for a
// given list block are sensitive that isn't captured in the individual
// blocks as they are processed independently). These maps allow the
// renderer to check the metadata on the overall groups and respond
// accordingly.
ReplaceBlocks map[string]bool
BeforeSensitiveBlocks map[string]bool
AfterSensitiveBlocks map[string]bool
}
func (blocks *Blocks) GetAllKeys() []string {
@ -53,23 +64,30 @@ func (blocks *Blocks) IsSetBlock(key string) bool {
return ok
}
func (blocks *Blocks) AddSingleBlock(key string, diff computed.Diff) {
func (blocks *Blocks) AddSingleBlock(key string, diff computed.Diff, replace, beforeSensitive, afterSensitive bool) {
blocks.SingleBlocks[key] = diff
blocks.ReplaceBlocks[key] = replace
blocks.BeforeSensitiveBlocks[key] = beforeSensitive
blocks.AfterSensitiveBlocks[key] = afterSensitive
}
func (blocks *Blocks) AddListBlock(key string, diff computed.Diff) {
blocks.ListBlocks[key] = append(blocks.ListBlocks[key], diff)
func (blocks *Blocks) AddAllListBlock(key string, diffs []computed.Diff, replace, beforeSensitive, afterSensitive bool) {
blocks.ListBlocks[key] = diffs
blocks.ReplaceBlocks[key] = replace
blocks.BeforeSensitiveBlocks[key] = beforeSensitive
blocks.AfterSensitiveBlocks[key] = afterSensitive
}
func (blocks *Blocks) AddSetBlock(key string, diff computed.Diff) {
blocks.SetBlocks[key] = append(blocks.SetBlocks[key], diff)
func (blocks *Blocks) AddAllSetBlock(key string, diffs []computed.Diff, replace, beforeSensitive, afterSensitive bool) {
blocks.SetBlocks[key] = diffs
blocks.ReplaceBlocks[key] = replace
blocks.BeforeSensitiveBlocks[key] = beforeSensitive
blocks.AfterSensitiveBlocks[key] = afterSensitive
}
func (blocks *Blocks) AddMapBlock(key string, entry string, diff computed.Diff) {
m := blocks.MapBlocks[key]
if m == nil {
m = make(map[string]computed.Diff)
}
m[entry] = diff
blocks.MapBlocks[key] = m
func (blocks *Blocks) AddAllMapBlocks(key string, diffs map[string]computed.Diff, replace, beforeSensitive, afterSensitive bool) {
blocks.MapBlocks[key] = diffs
blocks.ReplaceBlocks[key] = replace
blocks.BeforeSensitiveBlocks[key] = beforeSensitive
blocks.AfterSensitiveBlocks[key] = afterSensitive
}

View File

@ -58,6 +58,8 @@ func (renderer listRenderer) RenderHuman(diff computed.Diff, indent int, opts co
}
renderNext = false
opts := elementOpts
// If we want to display the context around this change, we want to
// render the change immediately before this change in the list, and the
// change immediately after in the list, even if both these changes are
@ -86,19 +88,26 @@ func (renderer listRenderer) RenderHuman(diff computed.Diff, indent int, opts co
// change that happened previously.
unchangedElements = nil
// As we also want to render the element immediately after any
// changes, we make a note here to say we should render the next
// change whatever it is. But, we only want to render the next
// change if the current change isn't a NoOp. If the current change
// is a NoOp then it was told to print by the last change and we
// don't want to cascade and print all changes from now on.
renderNext = element.Action != plans.NoOp
if element.Action == plans.NoOp {
// If this is a NoOp action then we're going to render it below
// so we need to just override the opts we're going to use to
// make sure we use the unchanged opts.
opts = unchangedElementOpts
} else {
// As we also want to render the element immediately after any
// changes, we make a note here to say we should render the next
// change whatever it is. But, we only want to render the next
// change if the current change isn't a NoOp. If the current change
// is a NoOp then it was told to print by the last change and we
// don't want to cascade and print all changes from now on.
renderNext = true
}
}
for _, warning := range element.WarningsHuman(indent+1, opts) {
buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning))
}
buf.WriteString(fmt.Sprintf("%s%s %s,\n", formatIndent(indent+1), colorizeDiffAction(element.Action, opts), element.RenderHuman(indent+1, elementOpts)))
buf.WriteString(fmt.Sprintf("%s%s %s,\n", formatIndent(indent+1), colorizeDiffAction(element.Action, opts), element.RenderHuman(indent+1, opts)))
}
// If we were not displaying any context alongside our changes then the

View File

@ -14,48 +14,63 @@ import (
var _ computed.DiffRenderer = (*mapRenderer)(nil)
func Map(elements map[string]computed.Diff) computed.DiffRenderer {
maximumKeyLen := 0
for key := range elements {
if maximumKeyLen < len(key) {
maximumKeyLen = len(key)
}
}
return &mapRenderer{
elements: elements,
maximumKeyLen: maximumKeyLen,
elements: elements,
}
}
func NestedMap(elements map[string]computed.Diff) computed.DiffRenderer {
return &mapRenderer{
elements: elements,
overrideNullSuffix: true,
overrideForcesReplacement: true,
}
}
type mapRenderer struct {
NoWarningsRenderer
elements map[string]computed.Diff
maximumKeyLen int
elements map[string]computed.Diff
overrideNullSuffix bool
overrideForcesReplacement bool
}
func (renderer mapRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string {
if len(renderer.elements) == 0 {
return fmt.Sprintf("{}%s%s", nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts))
}
forcesReplacementSelf := diff.Replace && !renderer.overrideForcesReplacement
forcesReplacementChildren := diff.Replace && renderer.overrideForcesReplacement
unchangedElements := 0
if len(renderer.elements) == 0 {
return fmt.Sprintf("{}%s%s", nullSuffix(diff.Action, opts), forcesReplacement(forcesReplacementSelf, opts))
}
// Sort the map elements by key, so we have a deterministic ordering in
// the output.
var keys []string
// We need to make sure the keys are capable of rendering properly.
escapedKeys := make(map[string]string)
maximumKeyLen := 0
for key := range renderer.elements {
keys = append(keys, key)
escapedKey := hclEscapeString(key)
escapedKeys[key] = escapedKey
if maximumKeyLen < len(escapedKey) {
maximumKeyLen = len(escapedKey)
}
}
sort.Strings(keys)
unchangedElements := 0
elementOpts := opts.Clone()
if diff.Action == plans.Delete {
elementOpts.OverrideNullSuffix = true
}
elementOpts.OverrideNullSuffix = diff.Action == plans.Delete || renderer.overrideNullSuffix
elementOpts.OverrideForcesReplacement = forcesReplacementChildren
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("{%s\n", forcesReplacement(diff.Replace, opts)))
buf.WriteString(fmt.Sprintf("{%s\n", forcesReplacement(forcesReplacementSelf, opts)))
for _, key := range keys {
element := renderer.elements[key]
@ -71,15 +86,11 @@ func (renderer mapRenderer) RenderHuman(diff computed.Diff, indent int, opts com
// Only show commas between elements for objects.
comma := ""
if _, ok := element.Renderer.(objectRenderer); ok {
if _, ok := element.Renderer.(*objectRenderer); ok {
comma = ","
}
// When we add padding for the keys, we want the length to be an
// additional 2 characters, as we are going to add quotation marks ("")
// around the key when it is rendered.
keyLenWithOffset := renderer.maximumKeyLen + 2
buf.WriteString(fmt.Sprintf("%s%s %-*q = %s%s\n", formatIndent(indent+1), colorizeDiffAction(element.Action, opts), keyLenWithOffset, key, element.RenderHuman(indent+1, elementOpts), comma))
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s%s\n", formatIndent(indent+1), colorizeDiffAction(element.Action, opts), maximumKeyLen, escapedKeys[key], element.RenderHuman(indent+1, elementOpts), comma))
}
if unchangedElements > 0 {

View File

@ -64,6 +64,17 @@ func (renderer objectRenderer) RenderHuman(diff computed.Diff, indent int, opts
for _, key := range keys {
attribute := renderer.attributes[key]
if importantAttribute(key) {
importantAttributeOpts := attributeOpts.Clone()
importantAttributeOpts.ShowUnchangedChildren = true
for _, warning := range attribute.WarningsHuman(indent+1, importantAttributeOpts) {
buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning))
}
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", formatIndent(indent+1), colorizeDiffAction(attribute.Action, importantAttributeOpts), maximumKeyLen, escapedKeys[key], attribute.RenderHuman(indent+1, importantAttributeOpts)))
continue
}
if attribute.Action == plans.NoOp && !opts.ShowUnchangedChildren {
// Don't render NoOp operations when we are compact display.
unchangedAttributes++

View File

@ -92,12 +92,12 @@ func (renderer primitiveRenderer) renderStringDiff(diff computed.Diff, indent in
// We are creating a single multiline string, so let's split by the new
// line character. While we are doing this, we are going to insert our
// indents and make sure each line is formatted correctly.
lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s ", formatIndent(indent), format.DiffActionSymbol(plans.NoOp))), "\n")
lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s ", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp))), "\n")
// We now just need to do the same for the first entry in lines, because
// we split on the new line characters which won't have been at the
// beginning of the first line.
lines[0] = fmt.Sprintf("%s%s %s", formatIndent(indent), format.DiffActionSymbol(plans.NoOp), lines[0])
lines[0] = fmt.Sprintf("%s%s %s", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), lines[0])
case plans.Delete:
str := evaluatePrimitiveString(renderer.before, opts)
@ -112,12 +112,12 @@ func (renderer primitiveRenderer) renderStringDiff(diff computed.Diff, indent in
// We are creating a single multiline string, so let's split by the new
// line character. While we are doing this, we are going to insert our
// indents and make sure each line is formatted correctly.
lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s ", formatIndent(indent), format.DiffActionSymbol(plans.NoOp))), "\n")
lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s ", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp))), "\n")
// We now just need to do the same for the first entry in lines, because
// we split on the new line characters which won't have been at the
// beginning of the first line.
lines[0] = fmt.Sprintf("%s%s %s", formatIndent(indent), format.DiffActionSymbol(plans.NoOp), lines[0])
lines[0] = fmt.Sprintf("%s%s %s", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), lines[0])
default:
beforeString := evaluatePrimitiveString(renderer.before, opts)
afterString := evaluatePrimitiveString(renderer.after, opts)
@ -147,16 +147,16 @@ func (renderer primitiveRenderer) renderStringDiff(diff computed.Diff, indent in
processIndices := func(beforeIx, afterIx int) {
if beforeIx < 0 || beforeIx >= len(beforeLines) {
lines = append(lines, fmt.Sprintf("%s%s %s", formatIndent(indent), colorizeDiffAction(plans.Create, opts), afterLines[afterIx]))
lines = append(lines, fmt.Sprintf("%s%s %s", formatIndent(indent+1), colorizeDiffAction(plans.Create, opts), afterLines[afterIx]))
return
}
if afterIx < 0 || afterIx >= len(afterLines) {
lines = append(lines, fmt.Sprintf("%s%s %s", formatIndent(indent), colorizeDiffAction(plans.Delete, opts), beforeLines[beforeIx]))
lines = append(lines, fmt.Sprintf("%s%s %s", formatIndent(indent+1), colorizeDiffAction(plans.Delete, opts), beforeLines[beforeIx]))
return
}
lines = append(lines, fmt.Sprintf("%s%s %s", formatIndent(indent), format.DiffActionSymbol(plans.NoOp), beforeLines[beforeIx]))
lines = append(lines, fmt.Sprintf("%s%s %s", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), beforeLines[beforeIx]))
}
isObjType := func(_ string) bool {
return false
@ -167,17 +167,21 @@ func (renderer primitiveRenderer) renderStringDiff(diff computed.Diff, indent in
// We return early if we find non-multiline strings or JSON strings, so we
// know here that we just render the lines slice properly.
return fmt.Sprintf("<<-EOT%s\n%s\n%sEOT%s",
return fmt.Sprintf("<<-EOT%s\n%s\n%s%s EOT%s",
forcesReplacement(diff.Replace, opts),
strings.Join(lines, "\n"),
formatIndent(indent),
format.DiffActionSymbol(plans.NoOp),
nullSuffix(diff.Action, opts))
}
func (renderer primitiveRenderer) renderStringDiffAsJson(diff computed.Diff, indent int, opts computed.RenderHumanOpts, before evaluatedString, after evaluatedString) string {
jsonDiff := RendererJsonOpts().Transform(before.Json, after.Json)
action := diff.Action
var whitespace, replace string
jsonOpts := opts.Clone()
if jsonDiff.Action == plans.NoOp && diff.Action == plans.Update {
// Then this means we are rendering a whitespace only change. The JSON
// differ will have ignored the whitespace changes so that makes the
@ -188,16 +192,28 @@ func (renderer primitiveRenderer) renderStringDiffAsJson(diff computed.Diff, ind
} else {
whitespace = " # whitespace changes"
}
// Because we'd be showing no changes otherwise:
jsonOpts.ShowUnchangedChildren = true
// Whitespace changes should not appear as if edited.
action = plans.NoOp
} else {
// We only show the replace suffix if we didn't print something out
// about whitespace changes.
replace = forcesReplacement(diff.Replace, opts)
}
renderedJsonDiff := jsonDiff.RenderHuman(indent, opts)
renderedJsonDiff := jsonDiff.RenderHuman(indent+1, jsonOpts)
if diff.Action == plans.Create || diff.Action == plans.Delete {
// We don't display the '+' or '-' symbols on the JSON diffs, we should
// still display the '~' for an update action though.
action = plans.NoOp
}
if strings.Contains(renderedJsonDiff, "\n") {
return fmt.Sprintf("jsonencode(%s\n%s%s %s%s\n%s)", whitespace, formatIndent(indent), colorizeDiffAction(diff.Action, opts), renderedJsonDiff, replace, formatIndent(indent))
return fmt.Sprintf("jsonencode(%s\n%s%s %s%s\n%s)", whitespace, formatIndent(indent+1), colorizeDiffAction(action, opts), renderedJsonDiff, replace, formatIndent(indent+1))
}
return fmt.Sprintf("jsonencode(%s)%s%s", renderedJsonDiff, whitespace, replace)
}

View File

@ -82,9 +82,9 @@ func TestRenderers_Human(t *testing.T) {
},
expected: `
<<-EOT
hello
world
EOT
hello
world
EOT
`,
},
"primitive_multiline_string_delete": {
@ -94,9 +94,9 @@ EOT
},
expected: `
<<-EOT
hello
world
EOT -> null
hello
world
EOT -> null
`,
},
"primitive_multiline_string_update": {
@ -106,11 +106,11 @@ EOT -> null
},
expected: `
<<-EOT
hello
- old
+ new
world
EOT
hello
- old
+ new
world
EOT
`,
},
"primitive_json_string_create": {
@ -120,11 +120,11 @@ EOT
},
expected: `
jsonencode(
+ {
+ key_one = "value_one"
+ key_two = "value_two"
}
)
{
+ key_one = "value_one"
+ key_two = "value_two"
}
)
`,
},
"primitive_json_string_delete": {
@ -134,11 +134,11 @@ jsonencode(
},
expected: `
jsonencode(
- {
- key_one = "value_one"
- key_two = "value_two"
} -> null
)
{
- key_one = "value_one"
- key_two = "value_two"
} -> null
)
`,
},
"primitive_json_string_update": {
@ -148,11 +148,11 @@ jsonencode(
},
expected: `
jsonencode(
~ {
+ key_three = "value_three"
# (2 unchanged attributes hidden)
}
)
~ {
+ key_three = "value_three"
# (2 unchanged attributes hidden)
}
)
`,
},
"primitive_fake_json_string_update": {
@ -170,14 +170,14 @@ jsonencode(
},
expected: `
<<-EOT
hello
world
EOT -> jsonencode(
+ {
+ key_one = "value_one"
+ key_two = "value_two"
}
)
hello
world
EOT -> jsonencode(
{
+ key_one = "value_one"
+ key_two = "value_two"
}
)
`,
},
"primitive_json_to_multiline_update": {
@ -187,14 +187,14 @@ EOT -> jsonencode(
},
expected: `
jsonencode(
- {
- key_one = "value_one"
- key_two = "value_two"
}
) -> <<-EOT
hello
world
EOT
{
- key_one = "value_one"
- key_two = "value_two"
}
) -> <<-EOT
hello
world
EOT
`,
},
"primitive_json_to_string_update": {
@ -204,11 +204,11 @@ EOT
},
expected: `
jsonencode(
- {
- key_one = "value_one"
- key_two = "value_two"
}
) -> "hello world"
{
- key_one = "value_one"
- key_two = "value_two"
}
) -> "hello world"
`,
},
"primitive_string_to_json_update": {
@ -218,11 +218,11 @@ jsonencode(
},
expected: `
"hello world" -> jsonencode(
+ {
+ key_one = "value_one"
+ key_two = "value_two"
}
)
{
+ key_one = "value_one"
+ key_two = "value_two"
}
)
`,
},
"primitive_multi_to_single_update": {
@ -232,10 +232,10 @@ jsonencode(
},
expected: `
<<-EOT
- hello
- world
+ hello world
EOT
- hello
- world
+ hello world
EOT
`,
},
"primitive_single_to_multi_update": {
@ -245,10 +245,10 @@ EOT
},
expected: `
<<-EOT
- hello world
+ hello
+ world
EOT
- hello world
+ hello
+ world
EOT
`,
},
"sensitive_update": {
@ -259,7 +259,7 @@ EOT
}, true, true),
Action: plans.Update,
},
expected: "(sensitive)",
expected: "(sensitive value)",
},
"sensitive_update_replace": {
diff: computed.Diff{
@ -271,7 +271,7 @@ EOT
Action: plans.Update,
Replace: true,
},
expected: "(sensitive) # forces replacement",
expected: "(sensitive value) # forces replacement",
},
"computed_create": {
diff: computed.Diff{
@ -465,7 +465,7 @@ EOT
},
expected: `
{
+ attribute_one = (sensitive)
+ attribute_one = (sensitive value)
}
`,
},
@ -484,7 +484,7 @@ EOT
},
expected: `
{
~ attribute_one = (sensitive)
~ attribute_one = (sensitive value)
}
`,
},
@ -503,7 +503,7 @@ EOT
},
expected: `
{
- attribute_one = (sensitive)
- attribute_one = (sensitive value)
}
`,
},
@ -720,7 +720,7 @@ EOT
},
expected: `
{
+ "element_one" = (sensitive)
+ "element_one" = (sensitive value)
}
`,
},
@ -739,7 +739,7 @@ EOT
},
expected: `
{
~ "element_one" = (sensitive)
~ "element_one" = (sensitive value)
}
`,
},
@ -760,7 +760,7 @@ EOT
{
# Warning: this attribute value will no longer be marked as sensitive
# after applying this change. The value is unchanged.
~ "element_one" = (sensitive)
~ "element_one" = (sensitive value)
}
`,
},
@ -779,7 +779,7 @@ EOT
},
expected: `
{
- "element_one" = (sensitive) -> null
- "element_one" = (sensitive value) -> null
}
`,
},
@ -1034,7 +1034,7 @@ EOT
},
expected: `
[
+ (sensitive),
+ (sensitive value),
]
`,
},
@ -1053,7 +1053,7 @@ EOT
},
expected: `
[
- (sensitive),
- (sensitive value),
]
`,
},
@ -1072,7 +1072,7 @@ EOT
},
expected: `
[
~ (sensitive),
~ (sensitive value),
]
`,
},
@ -1093,7 +1093,7 @@ EOT
[
# Warning: this attribute value will be marked as sensitive and will not
# display in UI output after applying this change. The value is unchanged.
~ (sensitive),
~ (sensitive value),
]
`,
},
@ -1336,7 +1336,7 @@ EOT
},
expected: `
[
+ (sensitive),
+ (sensitive value),
]
`,
},
@ -1355,7 +1355,7 @@ EOT
},
expected: `
[
- (sensitive),
- (sensitive value),
]
`,
},
@ -1374,7 +1374,7 @@ EOT
},
expected: `
[
~ (sensitive),
~ (sensitive value),
]
`,
},
@ -1395,7 +1395,7 @@ EOT
[
# Warning: this attribute value will be marked as sensitive and will not
# display in UI output after applying this change.
~ (sensitive),
~ (sensitive value),
]
`,
},
@ -1439,9 +1439,7 @@ EOT
Renderer: Block(nil, Blocks{}),
Action: plans.Create,
},
expected: `
{
}`,
expected: "{}",
},
"create_populated_block": {
diff: computed.Diff{
@ -1586,6 +1584,7 @@ EOT
+ nested_block_two {
+ string = "two"
}
# (1 unchanged block hidden)
}`,
},
@ -1855,9 +1854,7 @@ EOT
Renderer: Block(nil, Blocks{}),
Action: plans.Delete,
},
expected: `
{
}`,
expected: "{}",
},
"block_escapes_keys": {
diff: computed.Diff{
@ -1952,6 +1949,7 @@ EOT
{
id = "root"
# (1 unchanged attribute hidden)
# (2 unchanged blocks hidden)
}`,
},
@ -1998,7 +1996,7 @@ EOT
t.Run(name, func(t *testing.T) {
opts := tc.opts.Clone()
opts.Colorize = colorize
opts.Colorize = &colorize
expected := strings.TrimSpace(tc.expected)
actual := tc.diff.RenderHuman(0, opts)

View File

@ -25,7 +25,7 @@ type sensitiveRenderer struct {
}
func (renderer sensitiveRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string {
return fmt.Sprintf("(sensitive)%s%s", nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts))
return fmt.Sprintf("(sensitive value)%s%s", nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts))
}
func (renderer sensitiveRenderer) WarningsHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) []string {

View File

@ -37,15 +37,9 @@ func (renderer sensitiveBlockRenderer) WarningsHuman(diff computed.Diff, indent
return []string{}
}
var warning string
if renderer.beforeSensitive {
warning = opts.Colorize.Color(fmt.Sprintf(" # [yellow]Warning[reset]: this block will no longer be marked as sensitive\n%s # after applying this change.", formatIndent(indent)))
return []string{opts.Colorize.Color(fmt.Sprintf(" # [yellow]Warning[reset]: this block will no longer be marked as sensitive\n%s # after applying this change.", formatIndent(indent)))}
} else {
warning = opts.Colorize.Color(fmt.Sprintf(" # [yellow]Warning[reset]: this block will be marked as sensitive and will not\n%s # display in UI output after applying this change.", formatIndent(indent)))
return []string{opts.Colorize.Color(fmt.Sprintf(" # [yellow]Warning[reset]: this block will be marked as sensitive and will not\n%s # display in UI output after applying this change.", formatIndent(indent)))}
}
if renderer.inner.Action == plans.NoOp {
return []string{fmt.Sprintf("%s The value is unchanged.", warning)}
}
return []string{warning}
}

View File

@ -55,15 +55,27 @@ func unchanged(keyword string, count int, opts computed.RenderHumanOpts) string
return opts.Colorize.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %ss hidden)[reset]", count, keyword))
}
// ensureValidAttributeName checks if `name` contains any HCL syntax and returns
// it surrounded by quotation marks if it does.
// ensureValidAttributeName checks if `name` contains any HCL syntax and calls
// and returns hclEscapeString.
func ensureValidAttributeName(name string) string {
if !hclsyntax.ValidIdentifier(name) {
return fmt.Sprintf("%q", name)
return hclEscapeString(name)
}
return name
}
// hclEscapeString formats the input string into a format that is safe for
// rendering within HCL.
//
// Note, this function doesn't actually do a very good job of this currently. We
// need to expose some internal functions from HCL in a future version and call
// them from here. For now, just use "%q" formatting.
func hclEscapeString(str string) string {
// TODO: Replace this with more complete HCL logic instead of the simple
// go workaround.
return fmt.Sprintf("%q", str)
}
func colorizeDiffAction(action plans.Action, opts computed.RenderHumanOpts) string {
return opts.Colorize.Color(format.DiffActionSymbol(action))
}

View File

@ -0,0 +1,83 @@
package jsonformat
import (
"sort"
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
"github.com/hashicorp/terraform/internal/command/jsonformat/differ"
"github.com/hashicorp/terraform/internal/command/jsonplan"
"github.com/hashicorp/terraform/internal/plans"
)
func precomputeDiffs(plan Plan) diffs {
diffs := diffs{
outputs: make(map[string]computed.Diff),
}
for _, drift := range plan.ResourceDrift {
schema := plan.ProviderSchemas[drift.ProviderName].ResourceSchemas[drift.Type]
diffs.drift = append(diffs.drift, diff{
change: drift,
diff: differ.FromJsonChange(drift.Change).ComputeDiffForBlock(schema.Block),
})
}
for _, change := range plan.ResourceChanges {
schema := plan.ProviderSchemas[change.ProviderName].ResourceSchemas[change.Type]
diffs.changes = append(diffs.changes, diff{
change: change,
diff: differ.FromJsonChange(change.Change).ComputeDiffForBlock(schema.Block),
})
}
for key, output := range plan.OutputChanges {
diffs.outputs[key] = differ.FromJsonChange(output).ComputeDiffForOutput()
}
less := func(drs []diff) func(i, j int) bool {
return func(i, j int) bool {
iA := drs[i].change.Address
jA := drs[j].change.Address
if iA == jA {
return drs[i].change.Deposed < drs[j].change.Deposed
}
return iA < jA
}
}
sort.Slice(diffs.drift, less(diffs.drift))
sort.Slice(diffs.changes, less(diffs.changes))
return diffs
}
type diffs struct {
drift []diff
changes []diff
outputs map[string]computed.Diff
}
func (d diffs) Empty() bool {
for _, change := range d.changes {
if change.diff.Action != plans.NoOp || change.Moved() {
return false
}
}
for _, output := range d.outputs {
if output.Action != plans.NoOp {
return false
}
}
return true
}
type diff struct {
change jsonplan.ResourceChange
diff computed.Diff
}
func (d diff) Moved() bool {
return len(d.change.PreviousAddress) > 0 && d.change.PreviousAddress != d.change.Address
}

View File

@ -24,6 +24,20 @@ func (change Change) ComputeDiffForBlock(block *jsonprovider.Block) computed.Dif
attributes := make(map[string]computed.Diff)
for key, attr := range block.Attributes {
childValue := blockValue.getChild(key)
// Empty strings in blocks should be considered null for legacy reasons.
// The SDK doesn't support null strings yet, so we work around this now.
if before, ok := childValue.Before.(string); ok && len(before) == 0 {
childValue.Before = nil
}
if after, ok := childValue.After.(string); ok && len(after) == 0 {
childValue.After = nil
}
// Always treat changes to blocks as implicit.
childValue.BeforeExplicit = false
childValue.AfterExplicit = false
childChange := childValue.ComputeDiffForAttribute(attr)
if childChange.Action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
// Don't record nil values at all in blocks.
@ -35,53 +49,54 @@ func (change Change) ComputeDiffForBlock(block *jsonprovider.Block) computed.Dif
}
blocks := renderers.Blocks{
SingleBlocks: make(map[string]computed.Diff),
ListBlocks: make(map[string][]computed.Diff),
SetBlocks: make(map[string][]computed.Diff),
MapBlocks: make(map[string]map[string]computed.Diff),
ReplaceBlocks: make(map[string]bool),
BeforeSensitiveBlocks: make(map[string]bool),
AfterSensitiveBlocks: make(map[string]bool),
SingleBlocks: make(map[string]computed.Diff),
ListBlocks: make(map[string][]computed.Diff),
SetBlocks: make(map[string][]computed.Diff),
MapBlocks: make(map[string]map[string]computed.Diff),
}
for key, blockType := range block.BlockTypes {
childValue := blockValue.getChild(key)
beforeSensitive := childValue.isBeforeSensitive()
afterSensitive := childValue.isAfterSensitive()
forcesReplacement := childValue.ReplacePaths.ForcesReplacement()
switch NestingMode(blockType.NestingMode) {
case nestingModeSet:
diffs, action := childValue.computeBlockDiffsAsSet(blockType.Block)
if action == plans.NoOp && childValue.Before == childValue.After {
if action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
// Don't record nil values in blocks.
continue
}
for _, diff := range diffs {
blocks.AddSetBlock(key, diff)
}
blocks.AddAllSetBlock(key, diffs, forcesReplacement, beforeSensitive, afterSensitive)
current = collections.CompareActions(current, action)
case nestingModeList:
diffs, action := childValue.computeBlockDiffsAsList(blockType.Block)
if action == plans.NoOp && childValue.Before == childValue.After {
if action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
// Don't record nil values in blocks.
continue
}
for _, diff := range diffs {
blocks.AddListBlock(key, diff)
}
blocks.AddAllListBlock(key, diffs, forcesReplacement, beforeSensitive, afterSensitive)
current = collections.CompareActions(current, action)
case nestingModeMap:
diffs, action := childValue.computeBlockDiffsAsMap(blockType.Block)
if action == plans.NoOp && childValue.Before == childValue.After {
if action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
// Don't record nil values in blocks.
continue
}
for dKey, diff := range diffs {
blocks.AddMapBlock(key, dKey, diff)
}
blocks.AddAllMapBlocks(key, diffs, forcesReplacement, beforeSensitive, afterSensitive)
current = collections.CompareActions(current, action)
case nestingModeSingle, nestingModeGroup:
diff := childValue.ComputeDiffForBlock(blockType.Block)
if diff.Action == plans.NoOp && childValue.Before == childValue.After {
if diff.Action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
// Don't record nil values in blocks.
continue
}
blocks.AddSingleBlock(key, diff)
blocks.AddSingleBlock(key, diff, forcesReplacement, beforeSensitive, afterSensitive)
current = collections.CompareActions(current, diff.Action)
default:
panic("unrecognized nesting mode: " + blockType.NestingMode)

View File

@ -82,9 +82,9 @@ type Change struct {
ReplacePaths replace.ForcesReplacement
}
// ValueFromJsonChange unmarshals the raw []byte values in the jsonplan.Change
// FromJsonChange unmarshals the raw []byte values in the jsonplan.Change
// structs into generic interface{} types that can be reasoned about.
func ValueFromJsonChange(change jsonplan.Change) Change {
func FromJsonChange(change jsonplan.Change) Change {
return Change{
Before: unmarshalGeneric(change.Before),
After: unmarshalGeneric(change.After),

View File

@ -156,8 +156,8 @@ func TestValue_ObjectAttributes(t *testing.T) {
"attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false),
}, plans.Delete, false), plans.Update, false),
validateNestedObject: renderers.ValidateUnknown(renderers.ValidateNestedObject(map[string]renderers.ValidateDiffFunction{
"attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false),
}, plans.Delete, false), plans.Update, false),
"attribute_one": renderers.ValidateUnknown(renderers.ValidatePrimitive("old", nil, plans.Delete, false), plans.Update, false),
}, plans.Update, false), plans.Update, false),
validateSetDiffs: &SetDiff{
Before: SetDiffEntry{
ObjectDiff: map[string]renderers.ValidateDiffFunction{
@ -451,20 +451,6 @@ func TestValue_ObjectAttributes(t *testing.T) {
},
validateAction: plans.Update,
validateReplace: false,
validateSetDiffs: &SetDiff{
Before: SetDiffEntry{
ObjectDiff: nil,
Action: plans.Delete,
Replace: false,
},
After: SetDiffEntry{
ObjectDiff: map[string]renderers.ValidateDiffFunction{
"attribute_one": renderers.ValidateUnknown(nil, plans.Create, false),
},
Action: plans.Create,
Replace: false,
},
},
},
"update_computed_attribute": {
input: Change{

View File

@ -26,7 +26,7 @@ func (change Change) computeAttributeDiffAsNestedMap(attributes map[string]*json
NestingMode: "single",
})
})
return computed.NewDiff(renderers.Map(elements), current, change.ReplacePaths.ForcesReplacement())
return computed.NewDiff(renderers.NestedMap(elements), current, change.ReplacePaths.ForcesReplacement())
}
func (change Change) computeBlockDiffsAsMap(block *jsonprovider.Block) (map[string]computed.Diff, plans.Action) {

View File

@ -6,6 +6,7 @@ import (
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
"github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers"
"github.com/hashicorp/terraform/internal/command/jsonprovider"
"github.com/hashicorp/terraform/internal/plans"
)
type CreateSensitiveRenderer func(computed.Diff, bool, bool) computed.DiffRenderer
@ -56,7 +57,14 @@ func (change Change) checkForSensitive(create CreateSensitiveRenderer, computedD
inner := computedDiff(value)
return computed.NewDiff(create(inner, beforeSensitive, afterSensitive), inner.Action, change.ReplacePaths.ForcesReplacement()), true
action := inner.Action
if action == plans.NoOp && beforeSensitive != afterSensitive {
// Let's override this, since it means the sensitive status has changed
// rather than the actual content of the value.
action = plans.Update
}
return computed.NewDiff(create(inner, beforeSensitive, afterSensitive), action, change.ReplacePaths.ForcesReplacement()), true
}
func (change Change) isBeforeSensitive() bool {

View File

@ -38,10 +38,6 @@ func (change Change) computeAttributeDiffAsNestedSet(attributes map[string]*json
}
func (change Change) computeBlockDiffsAsSet(block *jsonprovider.Block) ([]computed.Diff, plans.Action) {
// TODO(liamcervante): In the upcoming block PR, block sets should override
// the forces replacement setting. We can't do that here, so make sure it
// gets carried over into the diff renderer logic when set and list blocks
// are given separate behaviour.
var elements []computed.Diff
current := change.getDefaultActionForIteration()
change.processSet(func(value Change) {
@ -71,7 +67,7 @@ func (change Change) processSet(process func(value Change)) {
}
child := sliceValue.getChild(ix, jx)
if reflect.DeepEqual(child.Before, child.After) && child.isBeforeSensitive() == child.isAfterSensitive() && child.Unknown == nil {
if reflect.DeepEqual(child.Before, child.After) && child.isBeforeSensitive() == child.isAfterSensitive() && !child.isUnknown() {
matched = true
foundInBefore[ix] = jx
foundInAfter[jx] = ix

View File

@ -11,23 +11,39 @@ import (
)
func (change Change) checkForUnknownType(ctype cty.Type) (computed.Diff, bool) {
return change.checkForUnknown(func(value Change) computed.Diff {
return change.checkForUnknown(false, func(value Change) computed.Diff {
return value.computeDiffForType(ctype)
})
}
func (change Change) checkForUnknownNestedAttribute(attribute *jsonprovider.NestedType) (computed.Diff, bool) {
return change.checkForUnknown(func(value Change) computed.Diff {
// We want our child attributes to show up as computed instead of deleted.
// Let's populate that here.
childUnknown := make(map[string]interface{})
for key := range attribute.Attributes {
childUnknown[key] = true
}
return change.checkForUnknown(childUnknown, func(value Change) computed.Diff {
return value.computeDiffForNestedAttribute(attribute)
})
}
func (change Change) checkForUnknownBlock(block *jsonprovider.Block) (computed.Diff, bool) {
return change.checkForUnknown(func(value Change) computed.Diff {
// We want our child attributes to show up as computed instead of deleted.
// Let's populate that here.
childUnknown := make(map[string]interface{})
for key := range block.Attributes {
childUnknown[key] = true
}
return change.checkForUnknown(childUnknown, func(value Change) computed.Diff {
return value.ComputeDiffForBlock(block)
})
}
func (change Change) checkForUnknown(computeDiff func(value Change) computed.Diff) (computed.Diff, bool) {
func (change Change) checkForUnknown(childUnknown interface{}, computeDiff func(value Change) computed.Diff) (computed.Diff, bool) {
unknown := change.isUnknown()
if !unknown {
@ -50,6 +66,7 @@ func (change Change) checkForUnknown(computeDiff func(value Change) computed.Dif
beforeValue := Change{
Before: change.Before,
BeforeSensitive: change.BeforeSensitive,
Unknown: childUnknown,
}
return change.asDiff(renderers.Unknown(computeDiff(beforeValue))), true
}

View File

@ -1,29 +1,461 @@
package jsonformat
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/mitchellh/colorstring"
"github.com/hashicorp/terraform/internal/command/format"
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
"github.com/hashicorp/terraform/internal/command/jsonplan"
"github.com/hashicorp/terraform/internal/command/jsonprovider"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/terminal"
)
type RendererOpt int
const (
detectedDrift string = "drift"
proposedChange string = "change"
Errored RendererOpt = iota
CanNotApply
)
type Plan struct {
OutputChanges map[string]jsonplan.Change `json:"output_changes"`
ResourceChanges []jsonplan.ResourceChange `json:"resource_changes"`
ResourceDrift []jsonplan.ResourceChange `json:"resource_drift"`
ProviderSchemas map[string]*jsonprovider.Provider `json:"provider_schemas"`
PlanFormatVersion string `json:"plan_format_version"`
OutputChanges map[string]jsonplan.Change `json:"output_changes"`
ResourceChanges []jsonplan.ResourceChange `json:"resource_changes"`
ResourceDrift []jsonplan.ResourceChange `json:"resource_drift"`
ProviderFormatVersion string `json:"provider_format_version"`
ProviderSchemas map[string]*jsonprovider.Provider `json:"provider_schemas"`
}
type Renderer struct {
Streams *terminal.Streams
Colorize *colorstring.Colorize
RunningInAutomation bool
}
func (r Renderer) RenderPlan(plan Plan) {
panic("not implemented")
func (r Renderer) RenderHumanPlan(plan Plan, mode plans.Mode, opts ...RendererOpt) {
// TODO(liamcervante): Tidy up this detection of version differences, we
// should only report warnings when the plan is generated using a newer
// version then we are executing. We could also look into major vs minor
// version differences. This should work for alpha testing in the meantime.
if plan.PlanFormatVersion != jsonplan.FormatVersion || plan.ProviderFormatVersion != jsonprovider.FormatVersion {
r.Streams.Println(format.WordWrap(
r.Colorize.Color("\n[bold][red]Warning:[reset][bold] This plan was generated using a different version of Terraform, the diff presented here maybe missing representations of recent features."),
r.Streams.Stdout.Columns()))
}
checkOpts := func(target RendererOpt) bool {
for _, opt := range opts {
if opt == target {
return true
}
}
return false
}
diffs := precomputeDiffs(plan)
haveRefreshChanges := r.renderHumanDiffDrift(diffs, mode)
willPrintResourceChanges := false
counts := make(map[plans.Action]int)
var changes []diff
for _, diff := range diffs.changes {
action := jsonplan.UnmarshalActions(diff.change.Change.Actions)
if action == plans.NoOp && !diff.Moved() {
// Don't show anything for NoOp changes.
continue
}
if action == plans.Delete && diff.change.Mode != "managed" {
// Don't render anything for deleted data sources.
continue
}
changes = append(changes, diff)
willPrintResourceChanges = true
// Don't count move-only changes
if action != plans.NoOp {
counts[action]++
}
}
if len(changes) == 0 && len(diffs.outputs) == 0 {
// If we didn't find any changes to report at all then this is a
// "No changes" plan. How we'll present this depends on whether
// the plan is "applyable" and, if so, whether it had refresh changes
// that we already would've presented above.
if checkOpts(Errored) {
if haveRefreshChanges {
r.Streams.Print(format.HorizontalRule(r.Colorize, r.Streams.Stdout.Columns()))
r.Streams.Println()
}
r.Streams.Print(
r.Colorize.Color("\n[reset][bold][red]Planning failed.[reset][bold] Terraform encountered an error while generating this plan.[reset]\n\n"),
)
} else {
switch mode {
case plans.RefreshOnlyMode:
if haveRefreshChanges {
// We already generated a sufficient prompt about what will
// happen if applying this change above, so we don't need to
// say anything more.
return
}
r.Streams.Print(r.Colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure still matches the configuration.[reset]\n\n"))
r.Streams.Println(format.WordWrap(
"Terraform has checked that the real remote objects still match the result of your most recent changes, and found no differences.",
r.Streams.Stdout.Columns()))
case plans.DestroyMode:
if haveRefreshChanges {
r.Streams.Print(format.HorizontalRule(r.Colorize, r.Streams.Stdout.Columns()))
fmt.Fprintln(r.Streams.Stdout.File)
}
r.Streams.Print(r.Colorize.Color("\n[reset][bold][green]No changes.[reset][bold] No objects need to be destroyed.[reset]\n\n"))
r.Streams.Println(format.WordWrap(
"Either you have not created any objects yet or the existing objects were already deleted outside of Terraform.",
r.Streams.Stdout.Columns()))
default:
if haveRefreshChanges {
r.Streams.Print(format.HorizontalRule(r.Colorize, r.Streams.Stdout.Columns()))
r.Streams.Println("")
}
r.Streams.Print(
r.Colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure matches the configuration.[reset]\n\n"),
)
if haveRefreshChanges {
if !checkOpts(CanNotApply) {
// In this case, applying this plan will not change any
// remote objects but _will_ update the state to match what
// we detected during refresh, so we'll reassure the user
// about that.
r.Streams.Println(format.WordWrap(
"Your configuration already matches the changes detected above, so applying this plan will only update the state to include the changes detected above and won't change any real infrastructure.",
r.Streams.Stdout.Columns(),
))
} else {
// In this case we detected changes during refresh but this isn't
// a planning mode where we consider those to be applyable. The
// user must re-run in refresh-only mode in order to update the
// state to match the upstream changes.
suggestion := "."
if !r.RunningInAutomation {
// The normal message includes a specific command line to run.
suggestion = ":\n terraform apply -refresh-only"
}
r.Streams.Println(format.WordWrap(
"Your configuration already matches the changes detected above. If you'd like to update the Terraform state to match, create and apply a refresh-only plan"+suggestion,
r.Streams.Stdout.Columns(),
))
}
return
}
// If we get down here then we're just in the simple situation where
// the plan isn't applyable at all.
r.Streams.Println(format.WordWrap(
"Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.",
r.Streams.Stdout.Columns(),
))
}
}
}
if haveRefreshChanges {
r.Streams.Print(format.HorizontalRule(r.Colorize, r.Streams.Stdout.Columns()))
r.Streams.Println()
}
if willPrintResourceChanges {
r.Streams.Println("\nTerraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:")
if counts[plans.Create] > 0 {
r.Streams.Println(r.Colorize.Color(actionDescription(plans.Create)))
}
if counts[plans.Update] > 0 {
r.Streams.Println(r.Colorize.Color(actionDescription(plans.Update)))
}
if counts[plans.Delete] > 0 {
r.Streams.Println(r.Colorize.Color(actionDescription(plans.Delete)))
}
if counts[plans.DeleteThenCreate] > 0 {
r.Streams.Println(r.Colorize.Color(actionDescription(plans.DeleteThenCreate)))
}
if counts[plans.CreateThenDelete] > 0 {
r.Streams.Println(r.Colorize.Color(actionDescription(plans.CreateThenDelete)))
}
if counts[plans.Read] > 0 {
r.Streams.Println(r.Colorize.Color(actionDescription(plans.Read)))
}
}
if len(changes) > 0 {
if checkOpts(Errored) {
r.Streams.Printf("\nTerraform planned the following actions, but then encountered a problem:\n\n")
} else {
r.Streams.Printf("\nTerraform will perform the following actions:\n\n")
}
for _, change := range changes {
diff, render := r.renderHumanDiff(change, proposedChange)
if render {
fmt.Fprintln(r.Streams.Stdout.File)
r.Streams.Println(diff)
}
}
r.Streams.Printf(
r.Colorize.Color("\n[bold]Plan:[reset] %d to add, %d to change, %d to destroy.\n"),
counts[plans.Create]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete],
counts[plans.Update],
counts[plans.Delete]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete])
}
diff := r.renderHumanDiffOutputs(diffs.outputs)
if len(diff) > 0 {
r.Streams.Print("\nChanges to Outputs:\n")
r.Streams.Printf("%s\n", diff)
if len(counts) == 0 {
// If we have output changes but not resource changes then we
// won't have output any indication about the changes at all yet,
// so we need some extra context about what it would mean to
// apply a change that _only_ includes output changes.
r.Streams.Println(format.WordWrap(
"\nYou can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.",
r.Streams.Stdout.Columns()))
}
}
}
func (r Renderer) renderHumanDiffOutputs(outputs map[string]computed.Diff) string {
var rendered []string
var keys []string
for key := range outputs {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
output := outputs[key]
if output.Action != plans.NoOp {
rendered = append(rendered, fmt.Sprintf("%s %s = %s", r.Colorize.Color(format.DiffActionSymbol(output.Action)), key, output.RenderHuman(0, computed.NewRenderHumanOpts(r.Colorize))))
}
}
return strings.Join(rendered, "\n")
}
func (r Renderer) renderHumanDiffDrift(diffs diffs, mode plans.Mode) bool {
var drs []diff
if mode == plans.RefreshOnlyMode {
drs = diffs.drift
} else {
for _, dr := range diffs.drift {
// TODO(liamcervante): Look into if we have to keep filtering resource changes.
// For now we still want to remove the moved resources from here as
// they will show up in the regular changes.
if dr.diff.Action != plans.NoOp {
drs = append(drs, dr)
}
}
}
if len(drs) == 0 {
return false
}
if diffs.Empty() && mode != plans.RefreshOnlyMode {
return false
}
r.Streams.Print(r.Colorize.Color("\n[bold][cyan]Note:[reset][bold] Objects have changed outside of Terraform\n"))
r.Streams.Println()
r.Streams.Print(format.WordWrap(
"Terraform detected the following changes made outside of Terraform since the last \"terraform apply\" which may have affected this plan:\n",
r.Streams.Stdout.Columns()))
for _, drift := range drs {
diff, render := r.renderHumanDiff(drift, detectedDrift)
if render {
r.Streams.Println()
r.Streams.Println(diff)
}
}
return true
}
func (r Renderer) renderHumanDiff(diff diff, cause string) (string, bool) {
// Internally, our computed diffs can't tell the difference between a
// replace action (eg. CreateThenDestroy, DestroyThenCreate) and a simple
// update action. So, at the top most level we rely on the action provided
// by the plan itself instead of what we compute. Nested attributes and
// blocks however don't have the replace type of actions, so we can trust
// the computed actions of these.
action := jsonplan.UnmarshalActions(diff.change.Change.Actions)
if action == plans.NoOp && (len(diff.change.PreviousAddress) == 0 || diff.change.PreviousAddress == diff.change.Address) {
// Skip resource changes that have nothing interesting to say.
return "", false
}
var buf bytes.Buffer
buf.WriteString(r.Colorize.Color(resourceChangeComment(diff.change, action, cause)))
buf.WriteString(fmt.Sprintf("%s %s %s", r.Colorize.Color(format.DiffActionSymbol(action)), resourceChangeHeader(diff.change), diff.diff.RenderHuman(0, computed.NewRenderHumanOpts(r.Colorize))))
return buf.String(), true
}
func (r Renderer) RenderLog(message map[string]interface{}) {
panic("not implemented")
}
func resourceChangeComment(resource jsonplan.ResourceChange, action plans.Action, changeCause string) string {
var buf bytes.Buffer
dispAddr := resource.Address
if len(resource.Deposed) != 0 {
dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, resource.Deposed)
}
switch action {
case plans.Create:
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be created", dispAddr))
case plans.Read:
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be read during apply", dispAddr))
switch resource.ActionReason {
case jsonplan.ResourceInstanceReadBecauseConfigUnknown:
buf.WriteString("\n # (config refers to values not yet known)")
case jsonplan.ResourceInstanceReadBecauseDependencyPending:
buf.WriteString("\n # (depends on a resource or a module with changes pending)")
}
case plans.Update:
switch changeCause {
case proposedChange:
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be updated in-place", dispAddr))
case detectedDrift:
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] has changed", dispAddr))
default:
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] update (unknown reason %s)", dispAddr, changeCause))
}
case plans.CreateThenDelete, plans.DeleteThenCreate:
switch resource.ActionReason {
case jsonplan.ResourceInstanceReplaceBecauseTainted:
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] is tainted, so must be [bold][red]replaced[reset]", dispAddr))
case jsonplan.ResourceInstanceReplaceByRequest:
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]replaced[reset], as requested", dispAddr))
case jsonplan.ResourceInstanceReplaceByTriggers:
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]replaced[reset] due to changes in replace_triggered_by", dispAddr))
default:
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] must be [bold][red]replaced[reset]", dispAddr))
}
case plans.Delete:
switch changeCause {
case proposedChange:
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]destroyed[reset]", dispAddr))
case detectedDrift:
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] has been deleted", dispAddr))
default:
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] delete (unknown reason %s)", dispAddr, changeCause))
}
// We can sometimes give some additional detail about why we're
// proposing to delete. We show this as additional notes, rather than
// as additional wording in the main action statement, in an attempt
// to make the "will be destroyed" message prominent and consistent
// in all cases, for easier scanning of this often-risky action.
switch resource.ActionReason {
case jsonplan.ResourceInstanceDeleteBecauseNoResourceConfig:
buf.WriteString(fmt.Sprintf("\n # (because %s.%s is not in configuration)", resource.Type, resource.Name))
case jsonplan.ResourceInstanceDeleteBecauseNoMoveTarget:
buf.WriteString(fmt.Sprintf("\n # (because %s was moved to %s, which is not in configuration)", resource.PreviousAddress, resource.Address))
case jsonplan.ResourceInstanceDeleteBecauseNoModule:
// FIXME: Ideally we'd truncate addr.Module to reflect the earliest
// step that doesn't exist, so it's clearer which call this refers
// to, but we don't have enough information out here in the UI layer
// to decide that; only the "expander" in Terraform Core knows
// which module instance keys are actually declared.
buf.WriteString(fmt.Sprintf("\n # (because %s is not in configuration)", resource.ModuleAddress))
case jsonplan.ResourceInstanceDeleteBecauseWrongRepetition:
var index interface{}
if resource.Index != nil {
if err := json.Unmarshal(resource.Index, &index); err != nil {
panic(err)
}
}
// We have some different variations of this one
switch index.(type) {
case nil:
buf.WriteString("\n # (because resource uses count or for_each)")
case float64:
buf.WriteString("\n # (because resource does not use count)")
case string:
buf.WriteString("\n # (because resource does not use for_each)")
}
case jsonplan.ResourceInstanceDeleteBecauseCountIndex:
buf.WriteString(fmt.Sprintf("\n # (because index [%s] is out of range for count)", resource.Index))
case jsonplan.ResourceInstanceDeleteBecauseEachKey:
buf.WriteString(fmt.Sprintf("\n # (because key [%s] is not in for_each map)", resource.Index))
}
if len(resource.Deposed) != 0 {
// Some extra context about this unusual situation.
buf.WriteString("\n # (left over from a partially-failed replacement of this instance)")
}
case plans.NoOp:
if len(resource.PreviousAddress) > 0 && resource.PreviousAddress != resource.Address {
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] has moved to [bold]%s[reset]", resource.PreviousAddress, dispAddr))
break
}
fallthrough
default:
// should never happen, since the above is exhaustive
buf.WriteString(fmt.Sprintf("%s has an action the plan renderer doesn't support (this is a bug)", dispAddr))
}
buf.WriteString("\n")
if len(resource.PreviousAddress) > 0 && resource.PreviousAddress != resource.Address && action != plans.NoOp {
buf.WriteString(fmt.Sprintf(" # [reset](moved from %s)\n", resource.PreviousAddress))
}
return buf.String()
}
func resourceChangeHeader(change jsonplan.ResourceChange) string {
mode := "resource"
if change.Mode != "managed" {
mode = "data"
}
return fmt.Sprintf("%s \"%s\" \"%s\"", mode, change.Type, change.Name)
}
func actionDescription(action plans.Action) string {
switch action {
case plans.Create:
return " [green]+[reset] create"
case plans.Delete:
return " [red]-[reset] destroy"
case plans.Update:
return " [yellow]~[reset] update in-place"
case plans.CreateThenDelete:
return "[green]+[reset]/[red]-[reset] create replacement and then destroy"
case plans.DeleteThenCreate:
return "[red]-[reset]/[green]+[reset] destroy and then create replacement"
case plans.Read:
return " [cyan]<=[reset] read (data resources)"
default:
panic(fmt.Sprintf("unrecognized change type: %s", action.String()))
}
}

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
@ -23,7 +24,22 @@ import (
// FormatVersion represents the version of the json format and will be
// incremented for any change to this format that requires changes to a
// consuming parser.
const FormatVersion = "1.1"
const (
FormatVersion = "1.1"
ResourceInstanceReplaceBecauseCannotUpdate = "replace_because_cannot_update"
ResourceInstanceReplaceBecauseTainted = "replace_because_tainted"
ResourceInstanceReplaceByRequest = "replace_by_request"
ResourceInstanceReplaceByTriggers = "replace_by_triggers"
ResourceInstanceDeleteBecauseNoResourceConfig = "delete_because_no_resource_config"
ResourceInstanceDeleteBecauseWrongRepetition = "delete_because_wrong_repetition"
ResourceInstanceDeleteBecauseCountIndex = "delete_because_count_index"
ResourceInstanceDeleteBecauseEachKey = "delete_because_each_key"
ResourceInstanceDeleteBecauseNoModule = "delete_because_no_module"
ResourceInstanceDeleteBecauseNoMoveTarget = "delete_because_no_move_target"
ResourceInstanceReadBecauseConfigUnknown = "read_because_config_unknown"
ResourceInstanceReadBecauseDependencyPending = "read_because_dependency_pending"
)
// Plan is the top-level representation of the json format of a plan. It includes
// the complete config and current state.
@ -156,7 +172,7 @@ func Marshal(
}
}
}
output.ResourceDrift, err = output.marshalResourceChanges(driftedResources, schemas)
output.ResourceDrift, err = MarshalResourceChanges(driftedResources, schemas)
if err != nil {
return nil, fmt.Errorf("error in marshaling resource drift: %s", err)
}
@ -168,15 +184,14 @@ func Marshal(
// output.ResourceChanges
if p.Changes != nil {
output.ResourceChanges, err = output.marshalResourceChanges(p.Changes.Resources, schemas)
output.ResourceChanges, err = MarshalResourceChanges(p.Changes.Resources, schemas)
if err != nil {
return nil, fmt.Errorf("error in marshaling resource changes: %s", err)
}
}
// output.OutputChanges
err = output.marshalOutputChanges(p.Changes)
if err != nil {
if output.OutputChanges, err = MarshalOutputChanges(p.Changes); err != nil {
return nil, fmt.Errorf("error in marshaling output changes: %s", err)
}
@ -257,7 +272,13 @@ func (p *plan) marshalPlanVariables(vars map[string]plans.DynamicValue, decls ma
return nil
}
func (p *plan) marshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schemas *terraform.Schemas) ([]ResourceChange, error) {
// MarshalResourceChanges converts the provided internal representation of
// ResourceInstanceChangeSrc objects into the public structured JSON changes.
//
// This function is referenced directly from the structured renderer tests, to
// ensure parity between the renderers. It probably shouldn't be used anywhere
// else.
func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schemas *terraform.Schemas) ([]ResourceChange, error) {
var ret []ResourceChange
for _, rc := range resources {
@ -391,29 +412,29 @@ func (p *plan) marshalResourceChanges(resources []*plans.ResourceInstanceChangeS
case plans.ResourceInstanceChangeNoReason:
r.ActionReason = "" // will be omitted in output
case plans.ResourceInstanceReplaceBecauseCannotUpdate:
r.ActionReason = "replace_because_cannot_update"
r.ActionReason = ResourceInstanceReplaceBecauseCannotUpdate
case plans.ResourceInstanceReplaceBecauseTainted:
r.ActionReason = "replace_because_tainted"
r.ActionReason = ResourceInstanceReplaceBecauseTainted
case plans.ResourceInstanceReplaceByRequest:
r.ActionReason = "replace_by_request"
r.ActionReason = ResourceInstanceReplaceByRequest
case plans.ResourceInstanceReplaceByTriggers:
r.ActionReason = "replace_by_triggers"
r.ActionReason = ResourceInstanceReplaceByTriggers
case plans.ResourceInstanceDeleteBecauseNoResourceConfig:
r.ActionReason = "delete_because_no_resource_config"
r.ActionReason = ResourceInstanceDeleteBecauseNoResourceConfig
case plans.ResourceInstanceDeleteBecauseWrongRepetition:
r.ActionReason = "delete_because_wrong_repetition"
r.ActionReason = ResourceInstanceDeleteBecauseWrongRepetition
case plans.ResourceInstanceDeleteBecauseCountIndex:
r.ActionReason = "delete_because_count_index"
r.ActionReason = ResourceInstanceDeleteBecauseCountIndex
case plans.ResourceInstanceDeleteBecauseEachKey:
r.ActionReason = "delete_because_each_key"
r.ActionReason = ResourceInstanceDeleteBecauseEachKey
case plans.ResourceInstanceDeleteBecauseNoModule:
r.ActionReason = "delete_because_no_module"
r.ActionReason = ResourceInstanceDeleteBecauseNoModule
case plans.ResourceInstanceDeleteBecauseNoMoveTarget:
r.ActionReason = "delete_because_no_move_target"
r.ActionReason = ResourceInstanceDeleteBecauseNoMoveTarget
case plans.ResourceInstanceReadBecauseConfigUnknown:
r.ActionReason = "read_because_config_unknown"
r.ActionReason = ResourceInstanceReadBecauseConfigUnknown
case plans.ResourceInstanceReadBecauseDependencyPending:
r.ActionReason = "read_because_dependency_pending"
r.ActionReason = ResourceInstanceReadBecauseDependencyPending
default:
return nil, fmt.Errorf("resource %s has an unsupported action reason %s", r.Address, rc.ActionReason)
}
@ -429,13 +450,19 @@ func (p *plan) marshalResourceChanges(resources []*plans.ResourceInstanceChangeS
return ret, nil
}
func (p *plan) marshalOutputChanges(changes *plans.Changes) error {
// MarshalOutputChanges converts the provided internal representation of
// Changes objects into the structured JSON representation.
//
// This function is referenced directly from the structured renderer tests, to
// ensure parity between the renderers. It probably shouldn't be used anywhere
// else.
func MarshalOutputChanges(changes *plans.Changes) (map[string]Change, error) {
if changes == nil {
// Nothing to do!
return nil
return nil, nil
}
p.OutputChanges = make(map[string]Change, len(changes.Outputs))
outputChanges := make(map[string]Change, len(changes.Outputs))
for _, oc := range changes.Outputs {
// Skip output changes that are not from the root module.
@ -448,7 +475,7 @@ func (p *plan) marshalOutputChanges(changes *plans.Changes) error {
changeV, err := oc.Decode()
if err != nil {
return err
return nil, err
}
// We drop the marks from the change, as decoding is only an
// intermediate step to re-encode the values as json
@ -461,14 +488,14 @@ func (p *plan) marshalOutputChanges(changes *plans.Changes) error {
if changeV.Before != cty.NilVal {
before, err = ctyjson.Marshal(changeV.Before, changeV.Before.Type())
if err != nil {
return err
return nil, err
}
}
if changeV.After != cty.NilVal {
if changeV.After.IsWhollyKnown() {
after, err = ctyjson.Marshal(changeV.After, changeV.After.Type())
if err != nil {
return err
return nil, err
}
afterUnknown = cty.False
} else {
@ -478,7 +505,7 @@ func (p *plan) marshalOutputChanges(changes *plans.Changes) error {
} else {
after, err = ctyjson.Marshal(filteredAfter, filteredAfter.Type())
if err != nil {
return err
return nil, err
}
}
afterUnknown = unknownAsBool(changeV.After)
@ -495,7 +522,7 @@ func (p *plan) marshalOutputChanges(changes *plans.Changes) error {
}
sensitive, err := ctyjson.Marshal(outputSensitive, outputSensitive.Type())
if err != nil {
return err
return nil, err
}
a, _ := ctyjson.Marshal(afterUnknown, afterUnknown.Type())
@ -509,10 +536,10 @@ func (p *plan) marshalOutputChanges(changes *plans.Changes) error {
AfterSensitive: json.RawMessage(sensitive),
}
p.OutputChanges[oc.Addr.OutputValue.Name] = c
outputChanges[oc.Addr.OutputValue.Name] = c
}
return nil
return outputChanges, nil
}
func (p *plan) marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) error {
@ -698,6 +725,36 @@ func actionString(action string) []string {
}
}
// UnmarshalActions reverses the actionString function.
func UnmarshalActions(actions []string) plans.Action {
if len(actions) == 2 {
if actions[0] == "create" && actions[1] == "delete" {
return plans.CreateThenDelete
}
if actions[0] == "delete" && actions[1] == "create" {
return plans.DeleteThenCreate
}
}
if len(actions) == 1 {
switch actions[0] {
case "create":
return plans.Create
case "delete":
return plans.Delete
case "update":
return plans.Update
case "read":
return plans.Read
case "no-op":
return plans.NoOp
}
}
panic("unrecognized action slice: " + strings.Join(actions, ", "))
}
// encodePaths lossily encodes a cty.PathSet into an array of arrays of step
// values, such as:
//

View File

@ -400,15 +400,13 @@ func TestOutputs(t *testing.T) {
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
p := newPlan()
err := p.marshalOutputChanges(test.changes)
changes, err := MarshalOutputChanges(test.changes)
if err != nil {
t.Fatalf("unexpected err: %s", err)
}
if !cmp.Equal(p.OutputChanges, test.expected) {
t.Errorf("wrong result:\n %v\n", cmp.Diff(p.OutputChanges, test.expected))
if !cmp.Equal(changes, test.expected) {
t.Errorf("wrong result:\n %v\n", cmp.Diff(changes, test.expected))
}
})
}

View File

@ -31,13 +31,21 @@ func newProviders() *providers {
}
}
// MarshalForRenderer converts the provided internation representation of the
// schema into the public structured JSON versions.
//
// This is a format that can be read by the structured plan renderer.
func MarshalForRenderer(s *terraform.Schemas) map[string]*Provider {
schemas := make(map[string]*Provider, len(s.Providers))
for k, v := range s.Providers {
schemas[k.String()] = marshalProvider(v)
}
return schemas
}
func Marshal(s *terraform.Schemas) ([]byte, error) {
providers := newProviders()
for k, v := range s.Providers {
providers.Schemas[k.String()] = marshalProvider(v)
}
providers.Schemas = MarshalForRenderer(s)
ret, err := json.Marshal(providers)
return ret, err
}