blob: efc273692e91222ec4b93296ea0134f71ce59d42 [file] [log] [blame]
package renderers
import (
"fmt"
"math/big"
"strings"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/command/jsonformat/collections"
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
"github.com/hashicorp/terraform/internal/command/jsonformat/structured"
"github.com/hashicorp/terraform/internal/command/jsonformat/structured/attribute_path"
"github.com/hashicorp/terraform/internal/plans"
)
var _ computed.DiffRenderer = (*primitiveRenderer)(nil)
func Primitive(before, after interface{}, ctype cty.Type) computed.DiffRenderer {
return &primitiveRenderer{
before: before,
after: after,
ctype: ctype,
}
}
type primitiveRenderer struct {
NoWarningsRenderer
before interface{}
after interface{}
ctype cty.Type
}
func (renderer primitiveRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string {
if renderer.ctype == cty.String {
return renderer.renderStringDiff(diff, indent, opts)
}
beforeValue := renderPrimitiveValue(renderer.before, renderer.ctype, opts)
afterValue := renderPrimitiveValue(renderer.after, renderer.ctype, opts)
switch diff.Action {
case plans.Create:
return fmt.Sprintf("%s%s", afterValue, forcesReplacement(diff.Replace, opts))
case plans.Delete:
return fmt.Sprintf("%s%s%s", beforeValue, nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts))
case plans.NoOp:
return fmt.Sprintf("%s%s", beforeValue, forcesReplacement(diff.Replace, opts))
default:
return fmt.Sprintf("%s %s %s%s", beforeValue, opts.Colorize.Color("[yellow]->[reset]"), afterValue, forcesReplacement(diff.Replace, opts))
}
}
func renderPrimitiveValue(value interface{}, t cty.Type, opts computed.RenderHumanOpts) string {
if value == nil {
return opts.Colorize.Color("[dark_gray]null[reset]")
}
switch {
case t == cty.Bool:
if value.(bool) {
return "true"
}
return "false"
case t == cty.Number:
bf := big.NewFloat(value.(float64))
return bf.Text('f', -1)
default:
panic("unrecognized primitive type: " + t.FriendlyName())
}
}
func (renderer primitiveRenderer) renderStringDiff(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string {
// We process multiline strings at the end of the switch statement.
var lines []string
switch diff.Action {
case plans.Create, plans.NoOp:
str := evaluatePrimitiveString(renderer.after, opts)
if str.Json != nil {
if diff.Action == plans.NoOp {
return renderer.renderStringDiffAsJson(diff, indent, opts, str, str)
} else {
return renderer.renderStringDiffAsJson(diff, indent, opts, evaluatedString{}, str)
}
}
if !str.IsMultiline {
return fmt.Sprintf("%s%s", str.RenderSimple(), forcesReplacement(diff.Replace, opts))
}
// 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), 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), writeDiffActionSymbol(plans.NoOp, opts), lines[0])
case plans.Delete:
str := evaluatePrimitiveString(renderer.before, opts)
if str.IsNull {
// We don't put the null suffix (-> null) here because the final
// render or null -> null would look silly.
return fmt.Sprintf("%s%s", str.RenderSimple(), forcesReplacement(diff.Replace, opts))
}
if str.Json != nil {
return renderer.renderStringDiffAsJson(diff, indent, opts, str, evaluatedString{})
}
if !str.IsMultiline {
return fmt.Sprintf("%s%s%s", str.RenderSimple(), nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts))
}
// 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), 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), writeDiffActionSymbol(plans.NoOp, opts), lines[0])
default:
beforeString := evaluatePrimitiveString(renderer.before, opts)
afterString := evaluatePrimitiveString(renderer.after, opts)
if beforeString.Json != nil && afterString.Json != nil {
return renderer.renderStringDiffAsJson(diff, indent, opts, beforeString, afterString)
}
if beforeString.Json != nil || afterString.Json != nil {
// This means one of the strings is JSON and one isn't. We're going
// to be a little inefficient here, but we can just reuse another
// renderer for this so let's keep it simple.
return computed.NewDiff(
TypeChange(
computed.NewDiff(Primitive(renderer.before, nil, cty.String), plans.Delete, false),
computed.NewDiff(Primitive(nil, renderer.after, cty.String), plans.Create, false)),
diff.Action,
diff.Replace).RenderHuman(indent, opts)
}
if !beforeString.IsMultiline && !afterString.IsMultiline {
return fmt.Sprintf("%s %s %s%s", beforeString.RenderSimple(), opts.Colorize.Color("[yellow]->[reset]"), afterString.RenderSimple(), forcesReplacement(diff.Replace, opts))
}
beforeLines := strings.Split(beforeString.String, "\n")
afterLines := strings.Split(afterString.String, "\n")
processIndices := func(beforeIx, afterIx int) {
if beforeIx < 0 || beforeIx >= len(beforeLines) {
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), writeDiffActionSymbol(plans.Delete, opts), beforeLines[beforeIx]))
return
}
lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), beforeLines[beforeIx]))
}
isObjType := func(_ string) bool {
return false
}
collections.ProcessSlice(beforeLines, afterLines, processIndices, isObjType)
}
// 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%sEOT%s",
forcesReplacement(diff.Replace, opts),
strings.Join(lines, "\n"),
formatIndent(indent),
writeDiffActionSymbol(plans.NoOp, opts),
nullSuffix(diff.Action, opts))
}
func (renderer primitiveRenderer) renderStringDiffAsJson(diff computed.Diff, indent int, opts computed.RenderHumanOpts, before evaluatedString, after evaluatedString) string {
jsonDiff := RendererJsonOpts().Transform(structured.Change{
BeforeExplicit: diff.Action != plans.Create,
AfterExplicit: diff.Action != plans.Delete,
Before: before.Json,
After: after.Json,
Unknown: false,
BeforeSensitive: false,
AfterSensitive: false,
ReplacePaths: attribute_path.Empty(false),
RelevantAttributes: attribute_path.AlwaysMatcher(),
})
action := diff.Action
jsonOpts := opts.Clone()
jsonOpts.OverrideNullSuffix = true
var whitespace, replace string
if jsonDiff.Action == plans.NoOp && diff.Action == plans.Update {
// Then this means we are rendering a whitespace only change. The JSON
// differ will have ignored the whitespace changes so that makes the
// diff we are about to print out very confusing without extra
// explanation.
if diff.Replace {
whitespace = " # whitespace changes force replacement"
} else {
whitespace = " # whitespace changes"
}
// Because we'd be showing no changes otherwise:
jsonOpts.ShowUnchangedChildren = true
// Whitespace changes should not appear as if edited.
action = plans.NoOp
} else {
// We only show the replace suffix if we didn't print something out
// about whitespace changes.
replace = forcesReplacement(diff.Replace, opts)
}
renderedJsonDiff := jsonDiff.RenderHuman(indent+1, jsonOpts)
if diff.Action == plans.Create || diff.Action == plans.Delete {
// We don't display the '+' or '-' symbols on the JSON diffs, we should
// still display the '~' for an update action though.
action = plans.NoOp
}
if strings.Contains(renderedJsonDiff, "\n") {
return fmt.Sprintf("jsonencode(%s\n%s%s%s%s\n%s%s)%s", whitespace, formatIndent(indent+1), writeDiffActionSymbol(action, opts), renderedJsonDiff, replace, formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts), nullSuffix(diff.Action, opts))
}
return fmt.Sprintf("jsonencode(%s)%s%s", renderedJsonDiff, whitespace, replace)
}