mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 08:21:07 -06:00
Structured Renderer: use the new renderer when rendering the state in addition to the plan (#32629)
* Use the new renderer when rendering the state * remove confusing and unneeded comment
This commit is contained in:
parent
c70244426a
commit
d818d7850d
@ -87,6 +87,10 @@ type RenderHumanOpts struct {
|
||||
// given complex change, instead of hiding unchanged items and compressing
|
||||
// them into a single line.
|
||||
ShowUnchangedChildren bool
|
||||
|
||||
// HideDiffActionSymbols tells the renderer not to show the '+'/'-' symbols
|
||||
// and to skip the places where the symbols would result in an offset.
|
||||
HideDiffActionSymbols bool
|
||||
}
|
||||
|
||||
// NewRenderHumanOpts creates a new RenderHumanOpts struct with the required
|
||||
@ -105,6 +109,7 @@ func (opts RenderHumanOpts) Clone() RenderHumanOpts {
|
||||
|
||||
OverrideNullSuffix: opts.OverrideNullSuffix,
|
||||
ShowUnchangedChildren: opts.ShowUnchangedChildren,
|
||||
HideDiffActionSymbols: opts.HideDiffActionSymbols,
|
||||
|
||||
// OverrideForcesReplacement is a special case in that it doesn't
|
||||
// cascade. So each diff should decide independently whether it's direct
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
@ -80,7 +79,7 @@ func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts c
|
||||
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)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%-*s = %s\n", formatIndent(indent+1), writeDiffActionSymbol(attribute.Action, importantAttributeOpts), maximumAttributeKeyLen, key, attribute.RenderHuman(indent+1, importantAttributeOpts)))
|
||||
continue
|
||||
}
|
||||
if attribute.Action == plans.NoOp && !opts.ShowUnchangedChildren {
|
||||
@ -91,11 +90,11 @@ 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, attributeOpts), maximumAttributeKeyLen, escapedAttributeKeys[key], attribute.RenderHuman(indent+1, attributeOpts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%-*s = %s\n", formatIndent(indent+1), writeDiffActionSymbol(attribute.Action, attributeOpts), maximumAttributeKeyLen, escapedAttributeKeys[key], attribute.RenderHuman(indent+1, attributeOpts)))
|
||||
}
|
||||
|
||||
if unchangedAttributes > 0 {
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s\n", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), unchanged("attribute", unchangedAttributes, opts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("attribute", unchangedAttributes, opts)))
|
||||
}
|
||||
|
||||
blockKeys := renderer.blocks.GetAllKeys()
|
||||
@ -141,7 +140,7 @@ func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts c
|
||||
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, blockOpts), EnsureValidAttributeName(key), mapKey, diff.RenderHuman(indent+1, blockOpts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%s%s %s\n", formatIndent(indent+1), writeDiffActionSymbol(diff.Action, blockOpts), EnsureValidAttributeName(key), mapKey, diff.RenderHuman(indent+1, blockOpts)))
|
||||
|
||||
}
|
||||
|
||||
@ -174,9 +173,9 @@ func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts c
|
||||
}
|
||||
|
||||
if unchangedBlocks > 0 {
|
||||
buf.WriteString(fmt.Sprintf("\n%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), writeDiffActionSymbol(plans.NoOp, opts), unchanged("block", unchangedBlocks, opts)))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("%s%s }", formatIndent(indent), format.DiffActionSymbol(plans.NoOp)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s}", formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts)))
|
||||
return buf.String()
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
@ -72,14 +71,14 @@ func (renderer listRenderer) RenderHuman(diff computed.Diff, indent int, opts co
|
||||
// minus 1 as the most recent unchanged element will be printed out
|
||||
// in full.
|
||||
if len(unchangedElements) > 1 {
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s\n", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), unchanged("element", len(unchangedElements)-1, opts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("element", len(unchangedElements)-1, opts)))
|
||||
}
|
||||
// If our list of unchanged elements contains at least one entry,
|
||||
// we're going to print out the most recent change in full. That's
|
||||
// what happens here.
|
||||
if len(unchangedElements) > 0 {
|
||||
lastElement := unchangedElements[len(unchangedElements)-1]
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s,\n", formatIndent(indent+1), colorizeDiffAction(lastElement.Action, opts), lastElement.RenderHuman(indent+1, unchangedElementOpts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%s,\n", formatIndent(indent+1), writeDiffActionSymbol(lastElement.Action, unchangedElementOpts), lastElement.RenderHuman(indent+1, unchangedElementOpts)))
|
||||
}
|
||||
// We now reset the unchanged elements list, we've printed out a
|
||||
// count of all the elements we skipped so we start counting from
|
||||
@ -107,7 +106,7 @@ func (renderer listRenderer) RenderHuman(diff computed.Diff, indent int, opts co
|
||||
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, opts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%s,\n", formatIndent(indent+1), writeDiffActionSymbol(element.Action, opts), element.RenderHuman(indent+1, opts)))
|
||||
}
|
||||
|
||||
// If we were not displaying any context alongside our changes then the
|
||||
@ -117,9 +116,9 @@ func (renderer listRenderer) RenderHuman(diff computed.Diff, indent int, opts co
|
||||
// If we were displaying context, then this will contain any unchanged
|
||||
// elements since our last change, so we should also print it out.
|
||||
if len(unchangedElements) > 0 {
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s\n", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), unchanged("element", len(unchangedElements), opts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("element", len(unchangedElements), opts)))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("%s%s ]%s", formatIndent(indent), format.DiffActionSymbol(plans.NoOp), nullSuffix(diff.Action, opts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s]%s", formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts), nullSuffix(diff.Action, opts)))
|
||||
return buf.String()
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
@ -92,17 +91,17 @@ func (renderer mapRenderer) RenderHuman(diff computed.Diff, indent int, opts com
|
||||
}
|
||||
|
||||
if renderer.alignKeys {
|
||||
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))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%-*s = %s%s\n", formatIndent(indent+1), writeDiffActionSymbol(element.Action, elementOpts), maximumKeyLen, escapedKeys[key], element.RenderHuman(indent+1, elementOpts), comma))
|
||||
} else {
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s = %s%s\n", formatIndent(indent+1), colorizeDiffAction(element.Action, opts), escapedKeys[key], element.RenderHuman(indent+1, elementOpts), comma))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%s = %s%s\n", formatIndent(indent+1), writeDiffActionSymbol(element.Action, elementOpts), escapedKeys[key], element.RenderHuman(indent+1, elementOpts), comma))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if unchangedElements > 0 {
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s\n", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), unchanged("element", unchangedElements, opts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("element", unchangedElements, opts)))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("%s%s }%s", formatIndent(indent), format.DiffActionSymbol(plans.NoOp), nullSuffix(diff.Action, opts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s}%s", formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts), nullSuffix(diff.Action, opts)))
|
||||
return buf.String()
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
@ -71,7 +70,7 @@ func (renderer objectRenderer) RenderHuman(diff computed.Diff, indent int, opts
|
||||
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)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%-*s = %s\n", formatIndent(indent+1), writeDiffActionSymbol(attribute.Action, importantAttributeOpts), maximumKeyLen, escapedKeys[key], attribute.RenderHuman(indent+1, importantAttributeOpts)))
|
||||
continue
|
||||
}
|
||||
|
||||
@ -84,13 +83,13 @@ func (renderer objectRenderer) RenderHuman(diff computed.Diff, indent int, opts
|
||||
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), maximumKeyLen, escapedKeys[key], attribute.RenderHuman(indent+1, attributeOpts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%-*s = %s\n", formatIndent(indent+1), writeDiffActionSymbol(attribute.Action, attributeOpts), maximumKeyLen, escapedKeys[key], attribute.RenderHuman(indent+1, attributeOpts)))
|
||||
}
|
||||
|
||||
if unchangedAttributes > 0 {
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s\n", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), unchanged("attribute", unchangedAttributes, opts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("attribute", unchangedAttributes, opts)))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("%s%s }%s", formatIndent(indent), format.DiffActionSymbol(plans.NoOp), nullSuffix(diff.Action, opts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s}%s", formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts), nullSuffix(diff.Action, opts)))
|
||||
return buf.String()
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/collections"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/differ/attribute_path"
|
||||
@ -95,12 +94,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+1), format.DiffActionSymbol(plans.NoOp))), "\n")
|
||||
lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts))), "\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+1), format.DiffActionSymbol(plans.NoOp), lines[0])
|
||||
lines[0] = fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), lines[0])
|
||||
case plans.Delete:
|
||||
str := evaluatePrimitiveString(renderer.before, opts)
|
||||
|
||||
@ -115,12 +114,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+1), format.DiffActionSymbol(plans.NoOp))), "\n")
|
||||
lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts))), "\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+1), format.DiffActionSymbol(plans.NoOp), lines[0])
|
||||
lines[0] = fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), lines[0])
|
||||
default:
|
||||
beforeString := evaluatePrimitiveString(renderer.before, opts)
|
||||
afterString := evaluatePrimitiveString(renderer.after, opts)
|
||||
@ -150,16 +149,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+1), colorizeDiffAction(plans.Create, opts), afterLines[afterIx]))
|
||||
lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.Create, opts), afterLines[afterIx]))
|
||||
return
|
||||
}
|
||||
|
||||
if afterIx < 0 || afterIx >= len(afterLines) {
|
||||
lines = append(lines, fmt.Sprintf("%s%s %s", formatIndent(indent+1), colorizeDiffAction(plans.Delete, opts), beforeLines[beforeIx]))
|
||||
lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.Delete, opts), beforeLines[beforeIx]))
|
||||
return
|
||||
}
|
||||
|
||||
lines = append(lines, fmt.Sprintf("%s%s %s", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), beforeLines[beforeIx]))
|
||||
lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), beforeLines[beforeIx]))
|
||||
}
|
||||
isObjType := func(_ string) bool {
|
||||
return false
|
||||
@ -170,11 +169,11 @@ 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%s%s EOT%s",
|
||||
return fmt.Sprintf("<<-EOT%s\n%s\n%s%sEOT%s",
|
||||
forcesReplacement(diff.Replace, opts),
|
||||
strings.Join(lines, "\n"),
|
||||
formatIndent(indent),
|
||||
format.DiffActionSymbol(plans.NoOp),
|
||||
writeDiffActionSymbol(plans.NoOp, opts),
|
||||
nullSuffix(diff.Action, opts))
|
||||
}
|
||||
|
||||
@ -218,7 +217,7 @@ func (renderer primitiveRenderer) renderStringDiffAsJson(diff computed.Diff, ind
|
||||
}
|
||||
|
||||
if strings.Contains(renderedJsonDiff, "\n") {
|
||||
return fmt.Sprintf("jsonencode(%s\n%s%s %s%s\n%s)%s", whitespace, formatIndent(indent+1), colorizeDiffAction(action, opts), renderedJsonDiff, replace, formatIndent(indent+1), nullSuffix(diff.Action, opts))
|
||||
return fmt.Sprintf("jsonencode(%s\n%s%s%s%s\n%s)%s", whitespace, formatIndent(indent+1), writeDiffActionSymbol(action, opts), renderedJsonDiff, replace, formatIndent(indent+1), nullSuffix(diff.Action, opts))
|
||||
}
|
||||
return fmt.Sprintf("jsonencode(%s)%s%s", renderedJsonDiff, whitespace, replace)
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package renderers
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
@ -24,7 +23,7 @@ type sensitiveBlockRenderer struct {
|
||||
}
|
||||
|
||||
func (renderer sensitiveBlockRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string {
|
||||
cachedLinePrefix := fmt.Sprintf("%s%s ", formatIndent(indent), format.DiffActionSymbol(plans.NoOp))
|
||||
cachedLinePrefix := fmt.Sprintf("%s%s", formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts))
|
||||
return fmt.Sprintf("{%s\n%s # At least one attribute in this block is (or was) sensitive,\n%s # so its contents will not be displayed.\n%s}",
|
||||
forcesReplacement(diff.Replace, opts), cachedLinePrefix, cachedLinePrefix, cachedLinePrefix)
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
@ -61,13 +60,13 @@ func (renderer setRenderer) RenderHuman(diff computed.Diff, indent int, opts com
|
||||
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), writeDiffActionSymbol(element.Action, elementOpts), element.RenderHuman(indent+1, elementOpts)))
|
||||
}
|
||||
|
||||
if unchangedElements > 0 {
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s\n", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), unchanged("element", unchangedElements, opts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("element", unchangedElements, opts)))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("%s%s ]%s", formatIndent(indent), format.DiffActionSymbol(plans.NoOp), nullSuffix(diff.Action, opts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s]%s", formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts), nullSuffix(diff.Action, opts)))
|
||||
return buf.String()
|
||||
}
|
||||
|
@ -76,6 +76,15 @@ func hclEscapeString(str string) string {
|
||||
return fmt.Sprintf("%q", str)
|
||||
}
|
||||
|
||||
func colorizeDiffAction(action plans.Action, opts computed.RenderHumanOpts) string {
|
||||
return opts.Colorize.Color(format.DiffActionSymbol(action))
|
||||
// writeDiffActionSymbol writes out the symbols for the associated action, and
|
||||
// handles localized colorization of the symbol as well as indenting the symbol
|
||||
// to be 4 spaces wide.
|
||||
//
|
||||
// If the opts has HideDiffActionSymbols set then this function returns an empty
|
||||
// string.
|
||||
func writeDiffActionSymbol(action plans.Action, opts computed.RenderHumanOpts) string {
|
||||
if opts.HideDiffActionSymbols {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s ", opts.Colorize.Color(format.DiffActionSymbol(action)))
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/differ"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/differ/attribute_path"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonplan"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonstate"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
@ -43,7 +44,7 @@ func precomputeDiffs(plan Plan, mode plans.Mode) diffs {
|
||||
continue
|
||||
}
|
||||
|
||||
schema := plan.GetSchema(drift)
|
||||
schema := plan.getSchema(drift)
|
||||
diffs.drift = append(diffs.drift, diff{
|
||||
change: drift,
|
||||
diff: differ.FromJsonChange(drift.Change, relevantAttrs).ComputeDiffForBlock(schema.Block),
|
||||
@ -51,7 +52,7 @@ func precomputeDiffs(plan Plan, mode plans.Mode) diffs {
|
||||
}
|
||||
|
||||
for _, change := range plan.ResourceChanges {
|
||||
schema := plan.GetSchema(change)
|
||||
schema := plan.getSchema(change)
|
||||
diffs.changes = append(diffs.changes, diff{
|
||||
change: change,
|
||||
diff: differ.FromJsonChange(change.Change, attribute_path.AlwaysMatcher()).ComputeDiffForBlock(schema.Block),
|
||||
@ -72,7 +73,7 @@ func precomputeDiffs(plan Plan, mode plans.Mode) diffs {
|
||||
}
|
||||
|
||||
if left.Mode != right.Mode {
|
||||
return left.Mode == jsonplan.DataResourceMode
|
||||
return left.Mode == jsonstate.DataResourceMode
|
||||
}
|
||||
|
||||
if left.Address != right.Address {
|
||||
|
@ -13,7 +13,7 @@ func (change Change) ComputeDiffForAttribute(attribute *jsonprovider.Attribute)
|
||||
if attribute.AttributeNestedType != nil {
|
||||
return change.computeDiffForNestedAttribute(attribute.AttributeNestedType)
|
||||
}
|
||||
return change.computeDiffForType(unmarshalAttribute(attribute))
|
||||
return change.ComputeDiffForType(unmarshalAttribute(attribute))
|
||||
}
|
||||
|
||||
func (change Change) computeDiffForNestedAttribute(nested *jsonprovider.NestedType) computed.Diff {
|
||||
@ -39,7 +39,7 @@ func (change Change) computeDiffForNestedAttribute(nested *jsonprovider.NestedTy
|
||||
}
|
||||
}
|
||||
|
||||
func (change Change) computeDiffForType(ctype cty.Type) computed.Diff {
|
||||
func (change Change) ComputeDiffForType(ctype cty.Type) computed.Diff {
|
||||
if sensitive, ok := change.checkForSensitiveType(ctype); ok {
|
||||
return sensitive
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/differ/attribute_path"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonplan"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonstate"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
@ -101,6 +102,46 @@ func FromJsonChange(change jsonplan.Change, relevantAttributes attribute_path.Ma
|
||||
}
|
||||
}
|
||||
|
||||
// FromJsonResource unmarshals the raw values in the jsonstate.Resource structs
|
||||
// into generic interface{} types that can be reasoned about.
|
||||
func FromJsonResource(resource jsonstate.Resource) Change {
|
||||
return Change{
|
||||
// We model resource formatting as NoOps.
|
||||
Before: unwrapAttributeValues(resource.AttributeValues),
|
||||
After: unwrapAttributeValues(resource.AttributeValues),
|
||||
|
||||
// We have some sensitive values, but we don't have any unknown values.
|
||||
Unknown: false,
|
||||
BeforeSensitive: unmarshalGeneric(resource.SensitiveValues),
|
||||
AfterSensitive: unmarshalGeneric(resource.SensitiveValues),
|
||||
|
||||
// We don't display replacement data for resources, and all attributes
|
||||
// are relevant.
|
||||
ReplacePaths: attribute_path.Empty(false),
|
||||
RelevantAttributes: attribute_path.AlwaysMatcher(),
|
||||
}
|
||||
}
|
||||
|
||||
// FromJsonOutput unmarshals the raw values in the jsonstate.Output structs into
|
||||
// generic interface{} types that can be reasoned about.
|
||||
func FromJsonOutput(output jsonstate.Output) Change {
|
||||
return Change{
|
||||
// We model resource formatting as NoOps.
|
||||
Before: unmarshalGeneric(output.Value),
|
||||
After: unmarshalGeneric(output.Value),
|
||||
|
||||
// We have some sensitive values, but we don't have any unknown values.
|
||||
Unknown: false,
|
||||
BeforeSensitive: output.Sensitive,
|
||||
AfterSensitive: output.Sensitive,
|
||||
|
||||
// We don't display replacement data for resources, and all attributes
|
||||
// are relevant.
|
||||
ReplacePaths: attribute_path.Empty(false),
|
||||
RelevantAttributes: attribute_path.AlwaysMatcher(),
|
||||
}
|
||||
}
|
||||
|
||||
func (change Change) asDiff(renderer computed.DiffRenderer) computed.Diff {
|
||||
return computed.NewDiff(renderer, change.calculateChange(), change.ReplacePaths.Matches())
|
||||
}
|
||||
@ -172,3 +213,11 @@ func unmarshalGeneric(raw json.RawMessage) interface{} {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func unwrapAttributeValues(values jsonstate.AttributeValues) map[string]interface{} {
|
||||
out := make(map[string]interface{})
|
||||
for key, value := range values {
|
||||
out[key] = unmarshalGeneric(value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ func (change Change) computeAttributeDiffAsList(elementType cty.Type) computed.D
|
||||
// after.
|
||||
value.RelevantAttributes = attribute_path.AlwaysMatcher()
|
||||
|
||||
return value.computeDiffForType(elementType)
|
||||
return value.ComputeDiffForType(elementType)
|
||||
}
|
||||
|
||||
isObjType := func(_ interface{}) bool {
|
||||
|
@ -18,7 +18,7 @@ func (change Change) computeAttributeDiffAsMap(elementType cty.Type) computed.Di
|
||||
// Mark non-relevant attributes as unchanged.
|
||||
value = value.AsNoOp()
|
||||
}
|
||||
return value.computeDiffForType(elementType)
|
||||
return value.ComputeDiffForType(elementType)
|
||||
})
|
||||
return computed.NewDiff(renderers.Map(elements), current, change.ReplacePaths.Matches())
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
|
||||
func (change Change) computeAttributeDiffAsObject(attributes map[string]cty.Type) computed.Diff {
|
||||
attributeDiffs, action := processObject(change, attributes, func(value Change, ctype cty.Type) computed.Diff {
|
||||
return value.computeDiffForType(ctype)
|
||||
return value.ComputeDiffForType(ctype)
|
||||
})
|
||||
return computed.NewDiff(renderers.Object(attributeDiffs), action, change.ReplacePaths.Matches())
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ type CreateSensitiveRenderer func(computed.Diff, bool, bool) computed.DiffRender
|
||||
|
||||
func (change Change) checkForSensitiveType(ctype cty.Type) (computed.Diff, bool) {
|
||||
return change.checkForSensitive(renderers.Sensitive, func(value Change) computed.Diff {
|
||||
return value.computeDiffForType(ctype)
|
||||
return value.ComputeDiffForType(ctype)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ func (change Change) computeAttributeDiffAsSet(elementType cty.Type) computed.Di
|
||||
var elements []computed.Diff
|
||||
current := change.getDefaultActionForIteration()
|
||||
change.processSet(func(value Change) {
|
||||
element := value.computeDiffForType(elementType)
|
||||
element := value.ComputeDiffForType(elementType)
|
||||
elements = append(elements, element)
|
||||
current = collections.CompareActions(current, element.Action)
|
||||
})
|
||||
|
@ -18,7 +18,7 @@ func (change Change) computeAttributeDiffAsTuple(elementTypes []cty.Type) comput
|
||||
// Mark non-relevant attributes as unchanged.
|
||||
childValue = childValue.AsNoOp()
|
||||
}
|
||||
element := childValue.computeDiffForType(elementType)
|
||||
element := childValue.ComputeDiffForType(elementType)
|
||||
elements = append(elements, element)
|
||||
current = collections.CompareActions(current, element.Action)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
|
||||
func (change Change) checkForUnknownType(ctype cty.Type) (computed.Diff, bool) {
|
||||
return change.checkForUnknown(false, func(value Change) computed.Diff {
|
||||
return value.computeDiffForType(ctype)
|
||||
return value.ComputeDiffForType(ctype)
|
||||
})
|
||||
}
|
||||
func (change Change) checkForUnknownNestedAttribute(attribute *jsonprovider.NestedType) (computed.Diff, bool) {
|
||||
|
478
internal/command/jsonformat/plan.go
Normal file
478
internal/command/jsonformat/plan.go
Normal file
@ -0,0 +1,478 @@
|
||||
package jsonformat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonplan"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonstate"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
type PlanRendererOpt int
|
||||
|
||||
const (
|
||||
detectedDrift string = "drift"
|
||||
proposedChange string = "change"
|
||||
|
||||
Errored PlanRendererOpt = iota
|
||||
CanNotApply
|
||||
)
|
||||
|
||||
type Plan struct {
|
||||
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"`
|
||||
RelevantAttributes []jsonplan.ResourceAttr `json:"relevant_attributes"`
|
||||
|
||||
ProviderFormatVersion string `json:"provider_format_version"`
|
||||
ProviderSchemas map[string]*jsonprovider.Provider `json:"provider_schemas"`
|
||||
}
|
||||
|
||||
func (plan Plan) getSchema(change jsonplan.ResourceChange) *jsonprovider.Schema {
|
||||
switch change.Mode {
|
||||
case jsonstate.ManagedResourceMode:
|
||||
return plan.ProviderSchemas[change.ProviderName].ResourceSchemas[change.Type]
|
||||
case jsonstate.DataResourceMode:
|
||||
return plan.ProviderSchemas[change.ProviderName].DataSourceSchemas[change.Type]
|
||||
default:
|
||||
panic("found unrecognized resource mode: " + change.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...PlanRendererOpt) {
|
||||
checkOpts := func(target PlanRendererOpt) bool {
|
||||
for _, opt := range opts {
|
||||
if opt == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
diffs := precomputeDiffs(plan, mode)
|
||||
haveRefreshChanges := renderHumanDiffDrift(renderer, 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 != jsonstate.ManagedResourceMode {
|
||||
// Don't render anything for deleted data sources.
|
||||
continue
|
||||
}
|
||||
|
||||
changes = append(changes, diff)
|
||||
|
||||
// Don't count move-only changes
|
||||
if action != plans.NoOp {
|
||||
willPrintResourceChanges = true
|
||||
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 {
|
||||
renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns()))
|
||||
renderer.Streams.Println()
|
||||
}
|
||||
renderer.Streams.Print(
|
||||
renderer.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
|
||||
}
|
||||
|
||||
renderer.Streams.Print(renderer.Colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure still matches the configuration.[reset]\n\n"))
|
||||
renderer.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.",
|
||||
renderer.Streams.Stdout.Columns()))
|
||||
case plans.DestroyMode:
|
||||
if haveRefreshChanges {
|
||||
renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns()))
|
||||
fmt.Fprintln(renderer.Streams.Stdout.File)
|
||||
}
|
||||
renderer.Streams.Print(renderer.Colorize.Color("\n[reset][bold][green]No changes.[reset][bold] No objects need to be destroyed.[reset]\n\n"))
|
||||
renderer.Streams.Println(format.WordWrap(
|
||||
"Either you have not created any objects yet or the existing objects were already deleted outside of Terraform.",
|
||||
renderer.Streams.Stdout.Columns()))
|
||||
default:
|
||||
if haveRefreshChanges {
|
||||
renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns()))
|
||||
renderer.Streams.Println("")
|
||||
}
|
||||
renderer.Streams.Print(
|
||||
renderer.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.
|
||||
renderer.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.",
|
||||
renderer.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 !renderer.RunningInAutomation {
|
||||
// The normal message includes a specific command line to run.
|
||||
suggestion = ":\n terraform apply -refresh-only"
|
||||
}
|
||||
renderer.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,
|
||||
renderer.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.
|
||||
renderer.Streams.Println(format.WordWrap(
|
||||
"Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.",
|
||||
renderer.Streams.Stdout.Columns(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if haveRefreshChanges {
|
||||
renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns()))
|
||||
renderer.Streams.Println()
|
||||
}
|
||||
|
||||
if willPrintResourceChanges {
|
||||
renderer.Streams.Println(format.WordWrap(
|
||||
"\nTerraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:",
|
||||
renderer.Streams.Stdout.Columns()))
|
||||
if counts[plans.Create] > 0 {
|
||||
renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Create)))
|
||||
}
|
||||
if counts[plans.Update] > 0 {
|
||||
renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Update)))
|
||||
}
|
||||
if counts[plans.Delete] > 0 {
|
||||
renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Delete)))
|
||||
}
|
||||
if counts[plans.DeleteThenCreate] > 0 {
|
||||
renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.DeleteThenCreate)))
|
||||
}
|
||||
if counts[plans.CreateThenDelete] > 0 {
|
||||
renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.CreateThenDelete)))
|
||||
}
|
||||
if counts[plans.Read] > 0 {
|
||||
renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Read)))
|
||||
}
|
||||
}
|
||||
|
||||
if len(changes) > 0 {
|
||||
if checkOpts(Errored) {
|
||||
renderer.Streams.Printf("\nTerraform planned the following actions, but then encountered a problem:\n")
|
||||
} else {
|
||||
renderer.Streams.Printf("\nTerraform will perform the following actions:\n")
|
||||
}
|
||||
|
||||
for _, change := range changes {
|
||||
diff, render := renderHumanDiff(renderer, change, proposedChange)
|
||||
if render {
|
||||
fmt.Fprintln(renderer.Streams.Stdout.File)
|
||||
renderer.Streams.Println(diff)
|
||||
}
|
||||
}
|
||||
|
||||
renderer.Streams.Printf(
|
||||
renderer.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 := renderHumanDiffOutputs(renderer, diffs.outputs)
|
||||
if len(diff) > 0 {
|
||||
renderer.Streams.Print("\nChanges to Outputs:\n")
|
||||
renderer.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.
|
||||
renderer.Streams.Println(format.WordWrap(
|
||||
"\nYou can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.",
|
||||
renderer.Streams.Stdout.Columns()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renderHumanDiffOutputs(renderer Renderer, outputs map[string]computed.Diff) string {
|
||||
var rendered []string
|
||||
|
||||
var keys []string
|
||||
escapedKeys := make(map[string]string)
|
||||
var escapedKeyMaxLen int
|
||||
for key := range outputs {
|
||||
escapedKey := renderers.EnsureValidAttributeName(key)
|
||||
keys = append(keys, key)
|
||||
escapedKeys[key] = escapedKey
|
||||
if len(escapedKey) > escapedKeyMaxLen {
|
||||
escapedKeyMaxLen = len(escapedKey)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
output := outputs[key]
|
||||
if output.Action != plans.NoOp {
|
||||
rendered = append(rendered, fmt.Sprintf("%s %-*s = %s", renderer.Colorize.Color(format.DiffActionSymbol(output.Action)), escapedKeyMaxLen, escapedKeys[key], output.RenderHuman(0, computed.NewRenderHumanOpts(renderer.Colorize))))
|
||||
}
|
||||
}
|
||||
return strings.Join(rendered, "\n")
|
||||
}
|
||||
|
||||
func renderHumanDiffDrift(renderer Renderer, diffs diffs, mode plans.Mode) bool {
|
||||
var drs []diff
|
||||
|
||||
// In refresh-only mode, we show all resources marked as drifted,
|
||||
// including those which have moved without other changes. In other plan
|
||||
// modes, move-only changes will be rendered in the planned changes, so
|
||||
// we skip them here.
|
||||
|
||||
if mode == plans.RefreshOnlyMode {
|
||||
drs = diffs.drift
|
||||
} else {
|
||||
for _, dr := range diffs.drift {
|
||||
if dr.diff.Action != plans.NoOp {
|
||||
drs = append(drs, dr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(drs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the overall plan is empty, and it's not a refresh only plan then we
|
||||
// won't show any drift changes.
|
||||
if diffs.Empty() && mode != plans.RefreshOnlyMode {
|
||||
return false
|
||||
}
|
||||
|
||||
renderer.Streams.Print(renderer.Colorize.Color("\n[bold][cyan]Note:[reset][bold] Objects have changed outside of Terraform\n"))
|
||||
renderer.Streams.Println()
|
||||
renderer.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",
|
||||
renderer.Streams.Stdout.Columns()))
|
||||
|
||||
for _, drift := range drs {
|
||||
diff, render := renderHumanDiff(renderer, drift, detectedDrift)
|
||||
if render {
|
||||
renderer.Streams.Println()
|
||||
renderer.Streams.Println(diff)
|
||||
}
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case plans.RefreshOnlyMode:
|
||||
renderer.Streams.Println(format.WordWrap(
|
||||
"\n\nThis is a refresh-only plan, so Terraform will not take any actions to undo these. If you were expecting these changes then you can apply this plan to record the updated values in the Terraform state without changing any remote objects.",
|
||||
renderer.Streams.Stdout.Columns(),
|
||||
))
|
||||
default:
|
||||
renderer.Streams.Println(format.WordWrap(
|
||||
"\n\nUnless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.",
|
||||
renderer.Streams.Stdout.Columns(),
|
||||
))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func renderHumanDiff(renderer Renderer, 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(renderer.Colorize.Color(resourceChangeComment(diff.change, action, cause)))
|
||||
buf.WriteString(fmt.Sprintf("%s %s %s", renderer.Colorize.Color(format.DiffActionSymbol(action)), resourceChangeHeader(diff.change), diff.diff.RenderHuman(0, computed.NewRenderHumanOpts(renderer.Colorize))))
|
||||
return buf.String(), true
|
||||
}
|
||||
|
||||
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 != jsonstate.ManagedResourceMode {
|
||||
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()))
|
||||
}
|
||||
}
|
@ -6607,7 +6607,7 @@ func runTestCases(t *testing.T, testCases map[string]testCase) {
|
||||
FromJsonChange(jsonchanges[0].Change, attribute_path.AlwaysMatcher()).
|
||||
ComputeDiffForBlock(jsonschemas[jsonchanges[0].ProviderName].ResourceSchemas[jsonchanges[0].Type].Block),
|
||||
}
|
||||
output, _ := renderer.renderHumanDiff(diff, proposedChange)
|
||||
output, _ := renderHumanDiff(renderer, diff, proposedChange)
|
||||
if diff := cmp.Diff(output, tc.ExpectedOutput); diff != "" {
|
||||
t.Errorf("wrong output\nexpected:\n%s\nactual:\n%s\ndiff:\n%s\n", tc.ExpectedOutput, output, diff)
|
||||
}
|
||||
@ -6726,7 +6726,7 @@ func TestOutputChanges(t *testing.T) {
|
||||
OutputChanges: outputs,
|
||||
}, plans.NormalMode)
|
||||
|
||||
output := renderer.renderHumanDiffOutputs(diffs.outputs)
|
||||
output := renderHumanDiffOutputs(renderer, diffs.outputs)
|
||||
if output != tc.output {
|
||||
t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.output)
|
||||
}
|
@ -1,55 +1,17 @@
|
||||
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/jsonformat/computed/renderers"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonplan"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonstate"
|
||||
"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 {
|
||||
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"`
|
||||
RelevantAttributes []jsonplan.ResourceAttr `json:"relevant_attributes"`
|
||||
|
||||
ProviderFormatVersion string `json:"provider_format_version"`
|
||||
ProviderSchemas map[string]*jsonprovider.Provider `json:"provider_schemas"`
|
||||
}
|
||||
|
||||
func (plan Plan) GetSchema(change jsonplan.ResourceChange) *jsonprovider.Schema {
|
||||
switch change.Mode {
|
||||
case jsonplan.ManagedResourceMode:
|
||||
return plan.ProviderSchemas[change.ProviderName].ResourceSchemas[change.Type]
|
||||
case jsonplan.DataResourceMode:
|
||||
return plan.ProviderSchemas[change.ProviderName].DataSourceSchemas[change.Type]
|
||||
default:
|
||||
panic("found unrecognized resource mode: " + change.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
type Renderer struct {
|
||||
Streams *terminal.Streams
|
||||
Colorize *colorstring.Colorize
|
||||
@ -57,445 +19,45 @@ type Renderer struct {
|
||||
RunningInAutomation bool
|
||||
}
|
||||
|
||||
func (r Renderer) RenderHumanPlan(plan Plan, mode plans.Mode, opts ...RendererOpt) {
|
||||
func (renderer Renderer) RenderHumanPlan(plan Plan, mode plans.Mode, opts ...PlanRendererOpt) {
|
||||
// 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()))
|
||||
renderer.Streams.Println(format.WordWrap(
|
||||
renderer.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."),
|
||||
renderer.Streams.Stdout.Columns()))
|
||||
}
|
||||
|
||||
checkOpts := func(target RendererOpt) bool {
|
||||
for _, opt := range opts {
|
||||
if opt == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
diffs := precomputeDiffs(plan, mode)
|
||||
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 != jsonplan.ManagedResourceMode {
|
||||
// Don't render anything for deleted data sources.
|
||||
continue
|
||||
}
|
||||
|
||||
changes = append(changes, diff)
|
||||
|
||||
// Don't count move-only changes
|
||||
if action != plans.NoOp {
|
||||
willPrintResourceChanges = true
|
||||
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(format.WordWrap(
|
||||
"\nTerraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:",
|
||||
r.Streams.Stdout.Columns()))
|
||||
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")
|
||||
} else {
|
||||
r.Streams.Printf("\nTerraform will perform the following actions:\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()))
|
||||
}
|
||||
}
|
||||
plan.renderHuman(renderer, mode, opts...)
|
||||
}
|
||||
|
||||
func (r Renderer) renderHumanDiffOutputs(outputs map[string]computed.Diff) string {
|
||||
var rendered []string
|
||||
|
||||
var keys []string
|
||||
escapedKeys := make(map[string]string)
|
||||
var escapedKeyMaxLen int
|
||||
for key := range outputs {
|
||||
escapedKey := renderers.EnsureValidAttributeName(key)
|
||||
keys = append(keys, key)
|
||||
escapedKeys[key] = escapedKey
|
||||
if len(escapedKey) > escapedKeyMaxLen {
|
||||
escapedKeyMaxLen = len(escapedKey)
|
||||
}
|
||||
func (renderer Renderer) RenderHumanState(state State) {
|
||||
// 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 state.StateFormatVersion != jsonstate.FormatVersion || state.ProviderFormatVersion != jsonprovider.FormatVersion {
|
||||
renderer.Streams.Println(format.WordWrap(
|
||||
renderer.Colorize.Color("\n[bold][red]Warning:[reset][bold] This state was retrieved using a different version of Terraform, the state presented here maybe missing representations of recent features."),
|
||||
renderer.Streams.Stdout.Columns()))
|
||||
}
|
||||
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)), escapedKeyMaxLen, escapedKeys[key], output.RenderHuman(0, computed.NewRenderHumanOpts(r.Colorize))))
|
||||
}
|
||||
if state.Empty() {
|
||||
renderer.Streams.Println("The state file is empty. No resources are represented.")
|
||||
return
|
||||
}
|
||||
return strings.Join(rendered, "\n")
|
||||
|
||||
opts := computed.RenderHumanOpts{
|
||||
ShowUnchangedChildren: true,
|
||||
HideDiffActionSymbols: true,
|
||||
}
|
||||
|
||||
state.renderHumanStateModule(renderer, state.RootModule, opts, true)
|
||||
state.renderHumanStateOutputs(renderer, opts)
|
||||
}
|
||||
|
||||
func (r Renderer) renderHumanDiffDrift(diffs diffs, mode plans.Mode) bool {
|
||||
var drs []diff
|
||||
|
||||
// In refresh-only mode, we show all resources marked as drifted,
|
||||
// including those which have moved without other changes. In other plan
|
||||
// modes, move-only changes will be rendered in the planned changes, so
|
||||
// we skip them here.
|
||||
|
||||
if mode == plans.RefreshOnlyMode {
|
||||
drs = diffs.drift
|
||||
} else {
|
||||
for _, dr := range diffs.drift {
|
||||
if dr.diff.Action != plans.NoOp {
|
||||
drs = append(drs, dr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(drs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the overall plan is empty, and it's not a refresh only plan then we
|
||||
// won't show any drift changes.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case plans.RefreshOnlyMode:
|
||||
r.Streams.Println(format.WordWrap(
|
||||
"\n\nThis is a refresh-only plan, so Terraform will not take any actions to undo these. If you were expecting these changes then you can apply this plan to record the updated values in the Terraform state without changing any remote objects.",
|
||||
r.Streams.Stdout.Columns(),
|
||||
))
|
||||
default:
|
||||
r.Streams.Println(format.WordWrap(
|
||||
"\n\nUnless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.",
|
||||
r.Streams.Stdout.Columns(),
|
||||
))
|
||||
}
|
||||
|
||||
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{}) {
|
||||
func (renderer 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 != jsonplan.ManagedResourceMode {
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
123
internal/command/jsonformat/state.go
Normal file
123
internal/command/jsonformat/state.go
Normal file
@ -0,0 +1,123 @@
|
||||
package jsonformat
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/differ"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonstate"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
StateFormatVersion string `json:"state_format_version"`
|
||||
RootModule jsonstate.Module `json:"root"`
|
||||
RootModuleOutputs map[string]jsonstate.Output `json:"root_module_outputs"`
|
||||
|
||||
ProviderFormatVersion string `json:"provider_format_version"`
|
||||
ProviderSchemas map[string]*jsonprovider.Provider `json:"provider_schemas"`
|
||||
}
|
||||
|
||||
func (state State) Empty() bool {
|
||||
return len(state.RootModuleOutputs) == 0 && len(state.RootModule.Resources) == 0 && len(state.RootModule.ChildModules) == 0
|
||||
}
|
||||
|
||||
func (state State) GetSchema(resource jsonstate.Resource) *jsonprovider.Schema {
|
||||
switch resource.Mode {
|
||||
case jsonstate.ManagedResourceMode:
|
||||
return state.ProviderSchemas[resource.ProviderName].ResourceSchemas[resource.Type]
|
||||
case jsonstate.DataResourceMode:
|
||||
return state.ProviderSchemas[resource.ProviderName].DataSourceSchemas[resource.Type]
|
||||
default:
|
||||
panic("found unrecognized resource mode: " + resource.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func (state State) renderHumanStateModule(renderer Renderer, module jsonstate.Module, opts computed.RenderHumanOpts, first bool) {
|
||||
// Sort the resources in the module first, for consistent output.
|
||||
var resources []jsonstate.Resource
|
||||
resources = append(resources, module.Resources...)
|
||||
sort.Slice(resources, func(i, j int) bool {
|
||||
left := resources[i]
|
||||
right := resources[j]
|
||||
|
||||
if left.Mode != right.Mode {
|
||||
return left.Mode == jsonstate.DataResourceMode
|
||||
}
|
||||
|
||||
if left.Address != right.Address {
|
||||
return left.Address < right.Address
|
||||
}
|
||||
|
||||
// Everything else being equal, we'll sort by deposed.
|
||||
return left.DeposedKey < right.DeposedKey
|
||||
})
|
||||
|
||||
if len(resources) > 0 && !first {
|
||||
renderer.Streams.Println()
|
||||
}
|
||||
|
||||
for _, resource := range resources {
|
||||
|
||||
if !first {
|
||||
renderer.Streams.Println()
|
||||
}
|
||||
|
||||
if first {
|
||||
first = false
|
||||
}
|
||||
|
||||
if len(resource.DeposedKey) > 0 {
|
||||
renderer.Streams.Printf("# %s: (deposed object %s)", resource.Address, resource.DeposedKey)
|
||||
} else if resource.Tainted {
|
||||
renderer.Streams.Printf("# %s: (tainted)", resource.Address)
|
||||
} else {
|
||||
renderer.Streams.Printf("# %s:", resource.Address)
|
||||
}
|
||||
|
||||
renderer.Streams.Println()
|
||||
|
||||
schema := state.GetSchema(resource)
|
||||
switch resource.Mode {
|
||||
case jsonstate.ManagedResourceMode:
|
||||
renderer.Streams.Printf("resource %q %q %s", resource.Type, resource.Name, differ.FromJsonResource(resource).ComputeDiffForBlock(schema.Block).RenderHuman(0, opts))
|
||||
case jsonstate.DataResourceMode:
|
||||
renderer.Streams.Printf("data %q %q %s", resource.Type, resource.Name, differ.FromJsonResource(resource).ComputeDiffForBlock(schema.Block).RenderHuman(0, opts))
|
||||
default:
|
||||
panic("found unrecognized resource mode: " + resource.Mode)
|
||||
}
|
||||
|
||||
renderer.Streams.Println()
|
||||
}
|
||||
|
||||
for _, child := range module.ChildModules {
|
||||
state.renderHumanStateModule(renderer, child, opts, first)
|
||||
}
|
||||
}
|
||||
|
||||
func (state State) renderHumanStateOutputs(renderer Renderer, opts computed.RenderHumanOpts) {
|
||||
|
||||
if len(state.RootModuleOutputs) > 0 {
|
||||
renderer.Streams.Printf("\n\nOutputs:\n\n")
|
||||
|
||||
var keys []string
|
||||
for key := range state.RootModuleOutputs {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
output := state.RootModuleOutputs[key]
|
||||
ctype, err := ctyjson.UnmarshalType(output.Type)
|
||||
if err != nil {
|
||||
// We can actually do this without the type, so even if we fail
|
||||
// to work out the type let's just render this anyway.
|
||||
renderer.Streams.Printf("%s = %s\n", key, differ.FromJsonOutput(state.RootModuleOutputs[key]).ComputeDiffForOutput().RenderHuman(0, opts))
|
||||
} else {
|
||||
renderer.Streams.Printf("%s = %s\n", key, differ.FromJsonOutput(state.RootModuleOutputs[key]).ComputeDiffForType(ctype).RenderHuman(0, opts))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
437
internal/command/jsonformat/state_test.go
Normal file
437
internal/command/jsonformat/state_test.go
Normal file
@ -0,0 +1,437 @@
|
||||
package jsonformat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/mitchellh/colorstring"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonstate"
|
||||
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||
"github.com/hashicorp/terraform/internal/terminal"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/hashicorp/terraform/internal/terraform"
|
||||
)
|
||||
|
||||
func TestState(t *testing.T) {
|
||||
color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
|
||||
|
||||
tests := []struct {
|
||||
State *format.StateOpts
|
||||
Want string
|
||||
}{
|
||||
{
|
||||
&format.StateOpts{
|
||||
State: &states.State{},
|
||||
Color: color,
|
||||
Schemas: &terraform.Schemas{},
|
||||
},
|
||||
"The state file is empty. No resources are represented.\n",
|
||||
},
|
||||
{
|
||||
&format.StateOpts{
|
||||
State: basicState(t),
|
||||
Color: color,
|
||||
Schemas: testSchemas(),
|
||||
},
|
||||
basicStateOutput,
|
||||
},
|
||||
{
|
||||
&format.StateOpts{
|
||||
State: nestedState(t),
|
||||
Color: color,
|
||||
Schemas: testSchemas(),
|
||||
},
|
||||
nestedStateOutput,
|
||||
},
|
||||
{
|
||||
&format.StateOpts{
|
||||
State: deposedState(t),
|
||||
Color: color,
|
||||
Schemas: testSchemas(),
|
||||
},
|
||||
deposedNestedStateOutput,
|
||||
},
|
||||
{
|
||||
&format.StateOpts{
|
||||
State: onlyDeposedState(t),
|
||||
Color: color,
|
||||
Schemas: testSchemas(),
|
||||
},
|
||||
onlyDeposedOutput,
|
||||
},
|
||||
{
|
||||
&format.StateOpts{
|
||||
State: stateWithMoreOutputs(t),
|
||||
Color: color,
|
||||
Schemas: testSchemas(),
|
||||
},
|
||||
stateWithMoreOutputsOutput,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
|
||||
root, outputs, err := jsonstate.MarshalForRenderer(&statefile.File{
|
||||
State: tt.State.State,
|
||||
}, tt.State.Schemas)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("found err: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
renderer := Renderer{
|
||||
Colorize: color,
|
||||
Streams: streams,
|
||||
}
|
||||
|
||||
renderer.RenderHumanState(State{
|
||||
StateFormatVersion: jsonstate.FormatVersion,
|
||||
RootModule: root,
|
||||
RootModuleOutputs: outputs,
|
||||
ProviderFormatVersion: jsonprovider.FormatVersion,
|
||||
ProviderSchemas: jsonprovider.MarshalForRenderer(tt.State.Schemas),
|
||||
})
|
||||
|
||||
result := done(t).All()
|
||||
if diff := cmp.Diff(result, tt.Want); diff != "" {
|
||||
t.Errorf("wrong output\nexpected:\n%s\nactual:\n%s\ndiff:\n%s\n", tt.Want, result, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testProvider() *terraform.MockProvider {
|
||||
p := new(terraform.MockProvider)
|
||||
p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
|
||||
return providers.ReadResourceResponse{NewState: req.PriorState}
|
||||
}
|
||||
|
||||
p.GetProviderSchemaResponse = testProviderSchema()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func testProviderSchema() *providers.GetProviderSchemaResponse {
|
||||
return &providers.GetProviderSchemaResponse{
|
||||
Provider: providers.Schema{
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"region": {Type: cty.String, Optional: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
ResourceTypes: map[string]providers.Schema{
|
||||
"test_resource": {
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Computed: true},
|
||||
"foo": {Type: cty.String, Optional: true},
|
||||
"woozles": {Type: cty.String, Optional: true},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"nested": {
|
||||
Nesting: configschema.NestingList,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"compute": {Type: cty.String, Optional: true},
|
||||
"value": {Type: cty.String, Optional: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
DataSources: map[string]providers.Schema{
|
||||
"test_data_source": {
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"compute": {Type: cty.String, Optional: true},
|
||||
"value": {Type: cty.String, Computed: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testSchemas() *terraform.Schemas {
|
||||
provider := testProvider()
|
||||
return &terraform.Schemas{
|
||||
Providers: map[addrs.Provider]*terraform.ProviderSchema{
|
||||
addrs.NewDefaultProvider("test"): provider.ProviderSchema(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const basicStateOutput = `# data.test_data_source.data:
|
||||
data "test_data_source" "data" {
|
||||
compute = "sure"
|
||||
}
|
||||
|
||||
# test_resource.baz[0]:
|
||||
resource "test_resource" "baz" {
|
||||
woozles = "confuzles"
|
||||
}
|
||||
|
||||
|
||||
Outputs:
|
||||
|
||||
bar = "bar value"
|
||||
`
|
||||
|
||||
const nestedStateOutput = `# test_resource.baz[0]:
|
||||
resource "test_resource" "baz" {
|
||||
woozles = "confuzles"
|
||||
|
||||
nested {
|
||||
value = "42"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const deposedNestedStateOutput = `# test_resource.baz[0]:
|
||||
resource "test_resource" "baz" {
|
||||
woozles = "confuzles"
|
||||
|
||||
nested {
|
||||
value = "42"
|
||||
}
|
||||
}
|
||||
|
||||
# test_resource.baz[0]: (deposed object 1234)
|
||||
resource "test_resource" "baz" {
|
||||
woozles = "confuzles"
|
||||
|
||||
nested {
|
||||
value = "42"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const onlyDeposedOutput = `# test_resource.baz[0]: (deposed object 1234)
|
||||
resource "test_resource" "baz" {
|
||||
woozles = "confuzles"
|
||||
|
||||
nested {
|
||||
value = "42"
|
||||
}
|
||||
}
|
||||
|
||||
# test_resource.baz[0]: (deposed object 5678)
|
||||
resource "test_resource" "baz" {
|
||||
woozles = "confuzles"
|
||||
|
||||
nested {
|
||||
value = "42"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const stateWithMoreOutputsOutput = `# test_resource.baz[0]:
|
||||
resource "test_resource" "baz" {
|
||||
woozles = "confuzles"
|
||||
}
|
||||
|
||||
|
||||
Outputs:
|
||||
|
||||
bool_var = true
|
||||
int_var = 42
|
||||
map_var = {
|
||||
"first" = "foo"
|
||||
"second" = "bar"
|
||||
}
|
||||
sensitive_var = (sensitive value)
|
||||
string_var = "string value"
|
||||
`
|
||||
|
||||
func basicState(t *testing.T) *states.State {
|
||||
state := states.NewState()
|
||||
|
||||
rootModule := state.RootModule()
|
||||
if rootModule == nil {
|
||||
t.Errorf("root module is nil; want valid object")
|
||||
}
|
||||
|
||||
rootModule.SetLocalValue("foo", cty.StringVal("foo value"))
|
||||
rootModule.SetOutputValue("bar", cty.StringVal("bar value"), false)
|
||||
rootModule.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_resource",
|
||||
Name: "baz",
|
||||
}.Instance(addrs.IntKey(0)),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
SchemaVersion: 0,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
rootModule.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.DataResourceMode,
|
||||
Type: "test_data_source",
|
||||
Name: "data",
|
||||
}.Instance(addrs.NoKey),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
SchemaVersion: 0,
|
||||
AttrsJSON: []byte(`{"compute":"sure"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
return state
|
||||
}
|
||||
|
||||
func stateWithMoreOutputs(t *testing.T) *states.State {
|
||||
state := states.NewState()
|
||||
|
||||
rootModule := state.RootModule()
|
||||
if rootModule == nil {
|
||||
t.Errorf("root module is nil; want valid object")
|
||||
}
|
||||
|
||||
rootModule.SetOutputValue("string_var", cty.StringVal("string value"), false)
|
||||
rootModule.SetOutputValue("int_var", cty.NumberIntVal(42), false)
|
||||
rootModule.SetOutputValue("bool_var", cty.BoolVal(true), false)
|
||||
rootModule.SetOutputValue("sensitive_var", cty.StringVal("secret!!!"), true)
|
||||
rootModule.SetOutputValue("map_var", cty.MapVal(map[string]cty.Value{
|
||||
"first": cty.StringVal("foo"),
|
||||
"second": cty.StringVal("bar"),
|
||||
}), false)
|
||||
|
||||
rootModule.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_resource",
|
||||
Name: "baz",
|
||||
}.Instance(addrs.IntKey(0)),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
SchemaVersion: 0,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
return state
|
||||
}
|
||||
|
||||
func nestedState(t *testing.T) *states.State {
|
||||
state := states.NewState()
|
||||
|
||||
rootModule := state.RootModule()
|
||||
if rootModule == nil {
|
||||
t.Errorf("root module is nil; want valid object")
|
||||
}
|
||||
|
||||
rootModule.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_resource",
|
||||
Name: "baz",
|
||||
}.Instance(addrs.IntKey(0)),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
SchemaVersion: 0,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles","nested": [{"value": "42"}]}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
return state
|
||||
}
|
||||
|
||||
func deposedState(t *testing.T) *states.State {
|
||||
state := nestedState(t)
|
||||
rootModule := state.RootModule()
|
||||
rootModule.SetResourceInstanceDeposed(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_resource",
|
||||
Name: "baz",
|
||||
}.Instance(addrs.IntKey(0)),
|
||||
states.DeposedKey("1234"),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
SchemaVersion: 0,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles","nested": [{"value": "42"}]}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
return state
|
||||
}
|
||||
|
||||
// replicate a corrupt resource where only a deposed exists
|
||||
func onlyDeposedState(t *testing.T) *states.State {
|
||||
state := states.NewState()
|
||||
|
||||
rootModule := state.RootModule()
|
||||
if rootModule == nil {
|
||||
t.Errorf("root module is nil; want valid object")
|
||||
}
|
||||
|
||||
rootModule.SetResourceInstanceDeposed(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_resource",
|
||||
Name: "baz",
|
||||
}.Instance(addrs.IntKey(0)),
|
||||
states.DeposedKey("1234"),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
SchemaVersion: 0,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles","nested": [{"value": "42"}]}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
rootModule.SetResourceInstanceDeposed(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_resource",
|
||||
Name: "baz",
|
||||
}.Instance(addrs.IntKey(0)),
|
||||
states.DeposedKey("5678"),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
SchemaVersion: 0,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles","nested": [{"value": "42"}]}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
return state
|
||||
}
|
@ -39,9 +39,6 @@ const (
|
||||
ResourceInstanceDeleteBecauseNoMoveTarget = "delete_because_no_move_target"
|
||||
ResourceInstanceReadBecauseConfigUnknown = "read_because_config_unknown"
|
||||
ResourceInstanceReadBecauseDependencyPending = "read_because_dependency_pending"
|
||||
|
||||
ManagedResourceMode = "managed"
|
||||
DataResourceMode = "data"
|
||||
)
|
||||
|
||||
// Plan is the top-level representation of the json format of a plan. It includes
|
||||
@ -448,9 +445,9 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema
|
||||
|
||||
switch addr.Resource.Resource.Mode {
|
||||
case addrs.ManagedResourceMode:
|
||||
r.Mode = ManagedResourceMode
|
||||
r.Mode = jsonstate.ManagedResourceMode
|
||||
case addrs.DataResourceMode:
|
||||
r.Mode = DataResourceMode
|
||||
r.Mode = jsonstate.DataResourceMode
|
||||
default:
|
||||
return nil, fmt.Errorf("resource %s has an unsupported mode %s", r.Address, addr.Resource.Resource.Mode.String())
|
||||
}
|
||||
|
@ -16,10 +16,15 @@ import (
|
||||
"github.com/hashicorp/terraform/internal/terraform"
|
||||
)
|
||||
|
||||
// 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.0"
|
||||
const (
|
||||
// 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.
|
||||
FormatVersion = "1.0"
|
||||
|
||||
ManagedResourceMode = "managed"
|
||||
DataResourceMode = "data"
|
||||
)
|
||||
|
||||
// state is the top-level representation of the json format of a terraform
|
||||
// state.
|
||||
@ -33,33 +38,33 @@ type state struct {
|
||||
// stateValues is the common representation of resolved values for both the prior
|
||||
// state (which is always complete) and the planned new state.
|
||||
type stateValues struct {
|
||||
Outputs map[string]output `json:"outputs,omitempty"`
|
||||
RootModule module `json:"root_module,omitempty"`
|
||||
Outputs map[string]Output `json:"outputs,omitempty"`
|
||||
RootModule Module `json:"root_module,omitempty"`
|
||||
}
|
||||
|
||||
type output struct {
|
||||
type Output struct {
|
||||
Sensitive bool `json:"sensitive"`
|
||||
Value json.RawMessage `json:"value,omitempty"`
|
||||
Type json.RawMessage `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// module is the representation of a module in state. This can be the root module
|
||||
// Module is the representation of a module in state. This can be the root module
|
||||
// or a child module
|
||||
type module struct {
|
||||
type Module struct {
|
||||
// Resources are sorted in a user-friendly order that is undefined at this
|
||||
// time, but consistent.
|
||||
Resources []resource `json:"resources,omitempty"`
|
||||
Resources []Resource `json:"resources,omitempty"`
|
||||
|
||||
// Address is the absolute module address, omitted for the root module
|
||||
Address string `json:"address,omitempty"`
|
||||
|
||||
// Each module object can optionally have its own nested "child_modules",
|
||||
// recursively describing the full module tree.
|
||||
ChildModules []module `json:"child_modules,omitempty"`
|
||||
ChildModules []Module `json:"child_modules,omitempty"`
|
||||
}
|
||||
|
||||
// Resource is the representation of a resource in the state.
|
||||
type resource struct {
|
||||
type Resource struct {
|
||||
// Address is the absolute resource address
|
||||
Address string `json:"address,omitempty"`
|
||||
|
||||
@ -70,7 +75,7 @@ type resource struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// Index is omitted for a resource not using `count` or `for_each`.
|
||||
Index addrs.InstanceKey `json:"index,omitempty"`
|
||||
Index json.RawMessage `json:"index,omitempty"`
|
||||
|
||||
// ProviderName allows the property "type" to be interpreted unambiguously
|
||||
// in the unusual situation where a provider offers a resource type whose
|
||||
@ -86,7 +91,7 @@ type resource struct {
|
||||
// resource, whose structure depends on the resource type schema. Any
|
||||
// unknown values are omitted or set to null, making them indistinguishable
|
||||
// from absent values.
|
||||
AttributeValues attributeValues `json:"values,omitempty"`
|
||||
AttributeValues AttributeValues `json:"values,omitempty"`
|
||||
|
||||
// SensitiveValues is similar to AttributeValues, but with all sensitive
|
||||
// values replaced with true, and all non-sensitive leaf values omitted.
|
||||
@ -103,11 +108,11 @@ type resource struct {
|
||||
DeposedKey string `json:"deposed_key,omitempty"`
|
||||
}
|
||||
|
||||
// attributeValues is the JSON representation of the attribute values of the
|
||||
// AttributeValues is the JSON representation of the attribute values of the
|
||||
// resource, whose structure depends on the resource type schema.
|
||||
type attributeValues map[string]interface{}
|
||||
type AttributeValues map[string]json.RawMessage
|
||||
|
||||
func marshalAttributeValues(value cty.Value) attributeValues {
|
||||
func marshalAttributeValues(value cty.Value) AttributeValues {
|
||||
// unmark our value to show all values
|
||||
value, _ = value.UnmarkDeep()
|
||||
|
||||
@ -115,7 +120,7 @@ func marshalAttributeValues(value cty.Value) attributeValues {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := make(attributeValues)
|
||||
ret := make(AttributeValues)
|
||||
|
||||
it := value.ElementIterator()
|
||||
for it.Next() {
|
||||
@ -133,6 +138,27 @@ func newState() *state {
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalForRenderer returns the pre-json encoding changes of the state, in a
|
||||
// format available to the structured renderer.
|
||||
func MarshalForRenderer(sf *statefile.File, schemas *terraform.Schemas) (Module, map[string]Output, error) {
|
||||
if sf.State.Modules == nil {
|
||||
// Empty state case.
|
||||
return Module{}, nil, nil
|
||||
}
|
||||
|
||||
outputs, err := MarshalOutputs(sf.State.RootModule().OutputValues)
|
||||
if err != nil {
|
||||
return Module{}, nil, err
|
||||
}
|
||||
|
||||
root, err := marshalRootModule(sf.State, schemas)
|
||||
if err != nil {
|
||||
return Module{}, nil, err
|
||||
}
|
||||
|
||||
return root, outputs, err
|
||||
}
|
||||
|
||||
// Marshal returns the json encoding of a terraform state.
|
||||
func Marshal(sf *statefile.File, schemas *terraform.Schemas) ([]byte, error) {
|
||||
output := newState()
|
||||
@ -181,14 +207,14 @@ func (jsonstate *state) marshalStateValues(s *states.State, schemas *terraform.S
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalOutputs translates a map of states.OutputValue to a map of jsonstate.output,
|
||||
// MarshalOutputs translates a map of states.OutputValue to a map of jsonstate.Output,
|
||||
// which are defined for json encoding.
|
||||
func MarshalOutputs(outputs map[string]*states.OutputValue) (map[string]output, error) {
|
||||
func MarshalOutputs(outputs map[string]*states.OutputValue) (map[string]Output, error) {
|
||||
if outputs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ret := make(map[string]output)
|
||||
ret := make(map[string]Output)
|
||||
for k, v := range outputs {
|
||||
ty := v.Value.Type()
|
||||
ov, err := ctyjson.Marshal(v.Value, ty)
|
||||
@ -199,7 +225,7 @@ func MarshalOutputs(outputs map[string]*states.OutputValue) (map[string]output,
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
ret[k] = output{
|
||||
ret[k] = Output{
|
||||
Value: ov,
|
||||
Type: ot,
|
||||
Sensitive: v.Sensitive,
|
||||
@ -209,8 +235,8 @@ func MarshalOutputs(outputs map[string]*states.OutputValue) (map[string]output,
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func marshalRootModule(s *states.State, schemas *terraform.Schemas) (module, error) {
|
||||
var ret module
|
||||
func marshalRootModule(s *states.State, schemas *terraform.Schemas) (Module, error) {
|
||||
var ret Module
|
||||
var err error
|
||||
|
||||
ret.Address = ""
|
||||
@ -259,11 +285,11 @@ func marshalModules(
|
||||
schemas *terraform.Schemas,
|
||||
modules []addrs.ModuleInstance,
|
||||
moduleMap map[string][]addrs.ModuleInstance,
|
||||
) ([]module, error) {
|
||||
var ret []module
|
||||
) ([]Module, error) {
|
||||
var ret []Module
|
||||
for _, child := range modules {
|
||||
// cm for child module, naming things is hard.
|
||||
cm := module{Address: child.String()}
|
||||
cm := Module{Address: child.String()}
|
||||
|
||||
// the module may be resourceless and contain only submodules, it will then be nil here
|
||||
stateMod := s.Module(child)
|
||||
@ -294,27 +320,34 @@ func marshalModules(
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func marshalResources(resources map[string]*states.Resource, module addrs.ModuleInstance, schemas *terraform.Schemas) ([]resource, error) {
|
||||
var ret []resource
|
||||
func marshalResources(resources map[string]*states.Resource, module addrs.ModuleInstance, schemas *terraform.Schemas) ([]Resource, error) {
|
||||
var ret []Resource
|
||||
|
||||
for _, r := range resources {
|
||||
for k, ri := range r.Instances {
|
||||
var err error
|
||||
|
||||
resAddr := r.Addr.Resource
|
||||
|
||||
current := resource{
|
||||
current := Resource{
|
||||
Address: r.Addr.Instance(k).String(),
|
||||
Index: k,
|
||||
Type: resAddr.Type,
|
||||
Name: resAddr.Name,
|
||||
ProviderName: r.ProviderConfig.Provider.String(),
|
||||
}
|
||||
|
||||
if k != nil {
|
||||
index := k.Value()
|
||||
if current.Index, err = ctyjson.Marshal(index, index.Type()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
switch resAddr.Mode {
|
||||
case addrs.ManagedResourceMode:
|
||||
current.Mode = "managed"
|
||||
current.Mode = ManagedResourceMode
|
||||
case addrs.DataResourceMode:
|
||||
current.Mode = "data"
|
||||
current.Mode = DataResourceMode
|
||||
default:
|
||||
return ret, fmt.Errorf("resource %s has an unsupported mode %s",
|
||||
resAddr.String(),
|
||||
@ -369,7 +402,7 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module
|
||||
|
||||
for deposedKey, rios := range ri.Deposed {
|
||||
// copy the base fields from the current instance
|
||||
deposed := resource{
|
||||
deposed := Resource{
|
||||
Address: current.Address,
|
||||
Type: current.Type,
|
||||
Name: current.Name,
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
func TestMarshalOutputs(t *testing.T) {
|
||||
tests := []struct {
|
||||
Outputs map[string]*states.OutputValue
|
||||
Want map[string]output
|
||||
Want map[string]Output
|
||||
Err bool
|
||||
}{
|
||||
{
|
||||
@ -33,7 +33,7 @@ func TestMarshalOutputs(t *testing.T) {
|
||||
Value: cty.StringVal("sekret"),
|
||||
},
|
||||
},
|
||||
map[string]output{
|
||||
map[string]Output{
|
||||
"test": {
|
||||
Sensitive: true,
|
||||
Value: json.RawMessage(`"sekret"`),
|
||||
@ -49,7 +49,7 @@ func TestMarshalOutputs(t *testing.T) {
|
||||
Value: cty.StringVal("not_so_sekret"),
|
||||
},
|
||||
},
|
||||
map[string]output{
|
||||
map[string]Output{
|
||||
"test": {
|
||||
Sensitive: false,
|
||||
Value: json.RawMessage(`"not_so_sekret"`),
|
||||
@ -76,7 +76,7 @@ func TestMarshalOutputs(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
},
|
||||
map[string]output{
|
||||
map[string]Output{
|
||||
"mapstring": {
|
||||
Sensitive: false,
|
||||
Value: json.RawMessage(`{"beep":"boop"}`),
|
||||
@ -111,7 +111,7 @@ func TestMarshalOutputs(t *testing.T) {
|
||||
func TestMarshalAttributeValues(t *testing.T) {
|
||||
tests := []struct {
|
||||
Attr cty.Value
|
||||
Want attributeValues
|
||||
Want AttributeValues
|
||||
}{
|
||||
{
|
||||
cty.NilVal,
|
||||
@ -125,13 +125,13 @@ func TestMarshalAttributeValues(t *testing.T) {
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar"),
|
||||
}),
|
||||
attributeValues{"foo": json.RawMessage(`"bar"`)},
|
||||
AttributeValues{"foo": json.RawMessage(`"bar"`)},
|
||||
},
|
||||
{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.NullVal(cty.String),
|
||||
}),
|
||||
attributeValues{"foo": json.RawMessage(`null`)},
|
||||
AttributeValues{"foo": json.RawMessage(`null`)},
|
||||
},
|
||||
{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
@ -143,7 +143,7 @@ func TestMarshalAttributeValues(t *testing.T) {
|
||||
cty.StringVal("moon"),
|
||||
}),
|
||||
}),
|
||||
attributeValues{
|
||||
AttributeValues{
|
||||
"bar": json.RawMessage(`{"hello":"world"}`),
|
||||
"baz": json.RawMessage(`["goodnight","moon"]`),
|
||||
},
|
||||
@ -159,7 +159,7 @@ func TestMarshalAttributeValues(t *testing.T) {
|
||||
cty.StringVal("moon").Mark(marks.Sensitive),
|
||||
}),
|
||||
}),
|
||||
attributeValues{
|
||||
AttributeValues{
|
||||
"bar": json.RawMessage(`{"hello":"world"}`),
|
||||
"baz": json.RawMessage(`["goodnight","moon"]`),
|
||||
},
|
||||
@ -180,7 +180,7 @@ func TestMarshalResources(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
Resources map[string]*states.Resource
|
||||
Schemas *terraform.Schemas
|
||||
Want []resource
|
||||
Want []Resource
|
||||
Err bool
|
||||
}{
|
||||
"nil": {
|
||||
@ -214,15 +214,15 @@ func TestMarshalResources(t *testing.T) {
|
||||
},
|
||||
},
|
||||
testSchemas(),
|
||||
[]resource{
|
||||
[]Resource{
|
||||
{
|
||||
Address: "test_thing.bar",
|
||||
Mode: "managed",
|
||||
Type: "test_thing",
|
||||
Name: "bar",
|
||||
Index: addrs.InstanceKey(nil),
|
||||
Index: nil,
|
||||
ProviderName: "registry.terraform.io/hashicorp/test",
|
||||
AttributeValues: attributeValues{
|
||||
AttributeValues: AttributeValues{
|
||||
"foozles": json.RawMessage(`null`),
|
||||
"woozles": json.RawMessage(`"confuzles"`),
|
||||
},
|
||||
@ -260,15 +260,15 @@ func TestMarshalResources(t *testing.T) {
|
||||
},
|
||||
},
|
||||
testSchemas(),
|
||||
[]resource{
|
||||
[]Resource{
|
||||
{
|
||||
Address: "test_thing.bar",
|
||||
Mode: "managed",
|
||||
Type: "test_thing",
|
||||
Name: "bar",
|
||||
Index: addrs.InstanceKey(nil),
|
||||
Index: nil,
|
||||
ProviderName: "registry.terraform.io/hashicorp/test",
|
||||
AttributeValues: attributeValues{
|
||||
AttributeValues: AttributeValues{
|
||||
"foozles": json.RawMessage(`"confuzles"`),
|
||||
"woozles": json.RawMessage(`null`),
|
||||
},
|
||||
@ -331,15 +331,15 @@ func TestMarshalResources(t *testing.T) {
|
||||
},
|
||||
},
|
||||
testSchemas(),
|
||||
[]resource{
|
||||
[]Resource{
|
||||
{
|
||||
Address: "test_thing.bar[0]",
|
||||
Mode: "managed",
|
||||
Type: "test_thing",
|
||||
Name: "bar",
|
||||
Index: addrs.IntKey(0),
|
||||
Index: json.RawMessage(`0`),
|
||||
ProviderName: "registry.terraform.io/hashicorp/test",
|
||||
AttributeValues: attributeValues{
|
||||
AttributeValues: AttributeValues{
|
||||
"foozles": json.RawMessage(`null`),
|
||||
"woozles": json.RawMessage(`"confuzles"`),
|
||||
},
|
||||
@ -373,15 +373,15 @@ func TestMarshalResources(t *testing.T) {
|
||||
},
|
||||
},
|
||||
testSchemas(),
|
||||
[]resource{
|
||||
[]Resource{
|
||||
{
|
||||
Address: "test_thing.bar[\"rockhopper\"]",
|
||||
Mode: "managed",
|
||||
Type: "test_thing",
|
||||
Name: "bar",
|
||||
Index: addrs.StringKey("rockhopper"),
|
||||
Index: json.RawMessage(`"rockhopper"`),
|
||||
ProviderName: "registry.terraform.io/hashicorp/test",
|
||||
AttributeValues: attributeValues{
|
||||
AttributeValues: AttributeValues{
|
||||
"foozles": json.RawMessage(`null`),
|
||||
"woozles": json.RawMessage(`"confuzles"`),
|
||||
},
|
||||
@ -417,16 +417,16 @@ func TestMarshalResources(t *testing.T) {
|
||||
},
|
||||
},
|
||||
testSchemas(),
|
||||
[]resource{
|
||||
[]Resource{
|
||||
{
|
||||
Address: "test_thing.bar",
|
||||
Mode: "managed",
|
||||
Type: "test_thing",
|
||||
Name: "bar",
|
||||
Index: addrs.InstanceKey(nil),
|
||||
Index: nil,
|
||||
ProviderName: "registry.terraform.io/hashicorp/test",
|
||||
DeposedKey: deposedKey.String(),
|
||||
AttributeValues: attributeValues{
|
||||
AttributeValues: AttributeValues{
|
||||
"foozles": json.RawMessage(`null`),
|
||||
"woozles": json.RawMessage(`"confuzles"`),
|
||||
},
|
||||
@ -466,15 +466,15 @@ func TestMarshalResources(t *testing.T) {
|
||||
},
|
||||
},
|
||||
testSchemas(),
|
||||
[]resource{
|
||||
[]Resource{
|
||||
{
|
||||
Address: "test_thing.bar",
|
||||
Mode: "managed",
|
||||
Type: "test_thing",
|
||||
Name: "bar",
|
||||
Index: addrs.InstanceKey(nil),
|
||||
Index: nil,
|
||||
ProviderName: "registry.terraform.io/hashicorp/test",
|
||||
AttributeValues: attributeValues{
|
||||
AttributeValues: AttributeValues{
|
||||
"foozles": json.RawMessage(`null`),
|
||||
"woozles": json.RawMessage(`"confuzles"`),
|
||||
},
|
||||
@ -485,10 +485,10 @@ func TestMarshalResources(t *testing.T) {
|
||||
Mode: "managed",
|
||||
Type: "test_thing",
|
||||
Name: "bar",
|
||||
Index: addrs.InstanceKey(nil),
|
||||
Index: nil,
|
||||
ProviderName: "registry.terraform.io/hashicorp/test",
|
||||
DeposedKey: deposedKey.String(),
|
||||
AttributeValues: attributeValues{
|
||||
AttributeValues: AttributeValues{
|
||||
"foozles": json.RawMessage(`null`),
|
||||
"woozles": json.RawMessage(`"confuzles"`),
|
||||
},
|
||||
@ -526,15 +526,15 @@ func TestMarshalResources(t *testing.T) {
|
||||
},
|
||||
},
|
||||
testSchemas(),
|
||||
[]resource{
|
||||
[]Resource{
|
||||
{
|
||||
Address: "test_map_attr.bar",
|
||||
Mode: "managed",
|
||||
Type: "test_map_attr",
|
||||
Name: "bar",
|
||||
Index: addrs.InstanceKey(nil),
|
||||
Index: nil,
|
||||
ProviderName: "registry.terraform.io/hashicorp/test",
|
||||
AttributeValues: attributeValues{
|
||||
AttributeValues: AttributeValues{
|
||||
"data": json.RawMessage(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
SensitiveValues: json.RawMessage(`{"data":true}`),
|
||||
|
@ -76,7 +76,7 @@ func TestShow_noArgsWithState(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
testingOverrides: metaOverridesForProvider(showFixtureProvider()),
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
@ -105,7 +105,7 @@ func TestShow_argsWithState(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
testingOverrides: metaOverridesForProvider(showFixtureProvider()),
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
@ -153,7 +153,7 @@ func TestShow_argsWithStateAliasedProvider(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
testingOverrides: metaOverridesForProvider(showFixtureProvider()),
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
@ -453,7 +453,7 @@ func TestShow_state(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
testingOverrides: metaOverridesForProvider(showFixtureProvider()),
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
@ -96,8 +96,9 @@ func (v *OperationHuman) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
||||
}
|
||||
|
||||
renderer := jsonformat.Renderer{
|
||||
Colorize: v.view.colorize,
|
||||
Streams: v.view.streams,
|
||||
Colorize: v.view.colorize,
|
||||
Streams: v.view.streams,
|
||||
RunningInAutomation: v.inAutomation,
|
||||
}
|
||||
|
||||
jplan := jsonformat.Plan{
|
||||
@ -111,7 +112,7 @@ func (v *OperationHuman) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
||||
}
|
||||
|
||||
// Side load some data that we can't extract from the JSON plan.
|
||||
var opts []jsonformat.RendererOpt
|
||||
var opts []jsonformat.PlanRendererOpt
|
||||
if !plan.CanApply() {
|
||||
opts = append(opts, jsonformat.CanNotApply)
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/arguments"
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonplan"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
@ -42,6 +41,12 @@ type ShowHuman struct {
|
||||
var _ Show = (*ShowHuman)(nil)
|
||||
|
||||
func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, stateFile *statefile.File, schemas *terraform.Schemas) int {
|
||||
renderer := jsonformat.Renderer{
|
||||
Colorize: v.view.colorize,
|
||||
Streams: v.view.streams,
|
||||
RunningInAutomation: v.view.runningInAutomation,
|
||||
}
|
||||
|
||||
if plan != nil {
|
||||
outputs, changed, drift, attrs, err := jsonplan.MarshalForRenderer(plan, schemas)
|
||||
if err != nil {
|
||||
@ -49,11 +54,6 @@ func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, stateFile
|
||||
return 1
|
||||
}
|
||||
|
||||
renderer := jsonformat.Renderer{
|
||||
Colorize: v.view.colorize,
|
||||
Streams: v.view.streams,
|
||||
}
|
||||
|
||||
jplan := jsonformat.Plan{
|
||||
PlanFormatVersion: jsonplan.FormatVersion,
|
||||
ProviderFormatVersion: jsonprovider.FormatVersion,
|
||||
@ -64,7 +64,7 @@ func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, stateFile
|
||||
RelevantAttributes: attrs,
|
||||
}
|
||||
|
||||
var opts []jsonformat.RendererOpt
|
||||
var opts []jsonformat.PlanRendererOpt
|
||||
if !plan.CanApply() {
|
||||
opts = append(opts, jsonformat.CanNotApply)
|
||||
}
|
||||
@ -79,11 +79,21 @@ func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, stateFile
|
||||
return 0
|
||||
}
|
||||
|
||||
v.view.streams.Println(format.State(&format.StateOpts{
|
||||
State: stateFile.State,
|
||||
Color: v.view.colorize,
|
||||
Schemas: schemas,
|
||||
}))
|
||||
root, outputs, err := jsonstate.MarshalForRenderer(stateFile, schemas)
|
||||
if err != nil {
|
||||
v.view.streams.Eprintf("Failed to marshal state to json: %s", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
jstate := jsonformat.State{
|
||||
StateFormatVersion: jsonstate.FormatVersion,
|
||||
ProviderFormatVersion: jsonprovider.FormatVersion,
|
||||
RootModule: root,
|
||||
RootModuleOutputs: outputs,
|
||||
ProviderSchemas: jsonprovider.MarshalForRenderer(schemas),
|
||||
}
|
||||
|
||||
renderer.RenderHumanState(jstate)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ func TestShowHuman(t *testing.T) {
|
||||
},
|
||||
testSchemas(),
|
||||
true,
|
||||
"\n",
|
||||
"The state file is empty. No resources are represented.\n",
|
||||
},
|
||||
"nothing": {
|
||||
nil,
|
||||
|
Loading…
Reference in New Issue
Block a user