mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 08:21:07 -06:00
Structured plan renderer: Implement the main functionality for the renderer (#32496)
* raw unmodified broken tests * tests execute, no panics * fix whitespace differences * fix all the tests * fix tests * actually fix tests * add missing plan metadata into the renderer * address comments * complete merge * remove TODO raising questions about outputs, they are fixed * missing bold on plan
This commit is contained in:
parent
af0ff90d6e
commit
95782f2491
@ -68,7 +68,7 @@ type DiffRenderer interface {
|
||||
// RenderHumanOpts contains options that can control how the human render
|
||||
// function of the DiffRenderer will function.
|
||||
type RenderHumanOpts struct {
|
||||
Colorize colorstring.Colorize
|
||||
Colorize *colorstring.Colorize
|
||||
|
||||
// OverrideNullSuffix tells the Renderer not to display the `-> null` suffix
|
||||
// that is normally displayed when an element, attribute, or block is
|
||||
@ -91,7 +91,7 @@ type RenderHumanOpts struct {
|
||||
|
||||
// NewRenderHumanOpts creates a new RenderHumanOpts struct with the required
|
||||
// fields set.
|
||||
func NewRenderHumanOpts(colorize colorstring.Colorize) RenderHumanOpts {
|
||||
func NewRenderHumanOpts(colorize *colorstring.Colorize) RenderHumanOpts {
|
||||
return RenderHumanOpts{
|
||||
Colorize: colorize,
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ var (
|
||||
|
||||
importantAttributes = []string{
|
||||
"id",
|
||||
"name",
|
||||
"tags",
|
||||
}
|
||||
)
|
||||
|
||||
@ -43,6 +45,10 @@ type blockRenderer struct {
|
||||
}
|
||||
|
||||
func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string {
|
||||
if len(renderer.attributes) == 0 && len(renderer.blocks.GetAllKeys()) == 0 {
|
||||
return fmt.Sprintf("{}%s", forcesReplacement(diff.Replace, opts))
|
||||
}
|
||||
|
||||
unchangedAttributes := 0
|
||||
unchangedBlocks := 0
|
||||
|
||||
@ -59,19 +65,24 @@ func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts c
|
||||
}
|
||||
sort.Strings(attributeKeys)
|
||||
|
||||
importantAttributeOpts := opts.Clone()
|
||||
importantAttributeOpts.ShowUnchangedChildren = true
|
||||
|
||||
attributeOpts := opts.Clone()
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(fmt.Sprintf("{%s\n", forcesReplacement(diff.Replace, opts)))
|
||||
for _, importantKey := range importantAttributes {
|
||||
if attribute, ok := renderer.attributes[importantKey]; ok {
|
||||
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", formatIndent(indent+1), colorizeDiffAction(attribute.Action, opts), maximumAttributeKeyLen, importantKey, attribute.RenderHuman(indent+1, opts)))
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range attributeKeys {
|
||||
attribute := renderer.attributes[key]
|
||||
if importantAttribute(key) {
|
||||
|
||||
// Always display the important attributes.
|
||||
for _, warning := range attribute.WarningsHuman(indent+1, importantAttributeOpts) {
|
||||
buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning))
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", formatIndent(indent+1), colorizeDiffAction(attribute.Action, importantAttributeOpts), maximumAttributeKeyLen, key, attribute.RenderHuman(indent+1, importantAttributeOpts)))
|
||||
continue
|
||||
}
|
||||
attribute := renderer.attributes[key]
|
||||
if attribute.Action == plans.NoOp && !opts.ShowUnchangedChildren {
|
||||
unchangedAttributes++
|
||||
continue
|
||||
@ -80,7 +91,7 @@ func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts c
|
||||
for _, warning := range attribute.WarningsHuman(indent+1, opts) {
|
||||
buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning))
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", formatIndent(indent+1), colorizeDiffAction(attribute.Action, opts), maximumAttributeKeyLen, escapedAttributeKeys[key], attribute.RenderHuman(indent+1, opts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", formatIndent(indent+1), colorizeDiffAction(attribute.Action, attributeOpts), maximumAttributeKeyLen, escapedAttributeKeys[key], attribute.RenderHuman(indent+1, attributeOpts)))
|
||||
}
|
||||
|
||||
if unchangedAttributes > 0 {
|
||||
@ -92,6 +103,22 @@ func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts c
|
||||
|
||||
foundChangedBlock := false
|
||||
renderBlock := func(diff computed.Diff, mapKey string, opts computed.RenderHumanOpts) {
|
||||
|
||||
creatingSensitiveValue := diff.Action == plans.Create && renderer.blocks.AfterSensitiveBlocks[key]
|
||||
deletingSensitiveValue := diff.Action == plans.Delete && renderer.blocks.BeforeSensitiveBlocks[key]
|
||||
modifyingSensitiveValue := (diff.Action == plans.Update || diff.Action == plans.NoOp) && (renderer.blocks.AfterSensitiveBlocks[key] || renderer.blocks.BeforeSensitiveBlocks[key])
|
||||
|
||||
if creatingSensitiveValue || deletingSensitiveValue || modifyingSensitiveValue {
|
||||
// Intercept the renderer here if the sensitive data was set
|
||||
// across all the blocks instead of individually.
|
||||
action := diff.Action
|
||||
if diff.Action == plans.NoOp && renderer.blocks.BeforeSensitiveBlocks[key] != renderer.blocks.AfterSensitiveBlocks[key] {
|
||||
action = plans.Update
|
||||
}
|
||||
|
||||
diff = computed.NewDiff(SensitiveBlock(diff, renderer.blocks.BeforeSensitiveBlocks[key], renderer.blocks.AfterSensitiveBlocks[key]), action, diff.Replace)
|
||||
}
|
||||
|
||||
if diff.Action == plans.NoOp && !opts.ShowUnchangedChildren {
|
||||
unchangedBlocks++
|
||||
return
|
||||
@ -104,10 +131,17 @@ func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts c
|
||||
foundChangedBlock = true
|
||||
}
|
||||
|
||||
for _, warning := range diff.WarningsHuman(indent+1, opts) {
|
||||
// If the force replacement metadata was set for every entry in the
|
||||
// block we need to override that here. Our child blocks will only
|
||||
// know about the replace function if it was set on them
|
||||
// specifically, and not if it was set for all the blocks.
|
||||
blockOpts := opts.Clone()
|
||||
blockOpts.OverrideForcesReplacement = renderer.blocks.ReplaceBlocks[key]
|
||||
|
||||
for _, warning := range diff.WarningsHuman(indent+1, blockOpts) {
|
||||
buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning))
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s%s %s\n", formatIndent(indent+1), colorizeDiffAction(diff.Action, opts), ensureValidAttributeName(key), mapKey, diff.RenderHuman(indent+1, opts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s%s %s\n", formatIndent(indent+1), colorizeDiffAction(diff.Action, blockOpts), ensureValidAttributeName(key), mapKey, diff.RenderHuman(indent+1, blockOpts)))
|
||||
|
||||
}
|
||||
|
||||
@ -140,7 +174,7 @@ func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts c
|
||||
}
|
||||
|
||||
if unchangedBlocks > 0 {
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s\n", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), unchanged("block", unchangedBlocks, opts)))
|
||||
buf.WriteString(fmt.Sprintf("\n%s%s %s\n", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), unchanged("block", unchangedBlocks, opts)))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("%s%s }", formatIndent(indent), format.DiffActionSymbol(plans.NoOp)))
|
||||
|
@ -13,6 +13,17 @@ type Blocks struct {
|
||||
ListBlocks map[string][]computed.Diff
|
||||
SetBlocks map[string][]computed.Diff
|
||||
MapBlocks map[string]map[string]computed.Diff
|
||||
|
||||
// ReplaceBlocks and Before/AfterSensitiveBlocks carry forward the
|
||||
// information about an entire group of blocks (eg. if all the blocks for a
|
||||
// given list block are sensitive that isn't captured in the individual
|
||||
// blocks as they are processed independently). These maps allow the
|
||||
// renderer to check the metadata on the overall groups and respond
|
||||
// accordingly.
|
||||
|
||||
ReplaceBlocks map[string]bool
|
||||
BeforeSensitiveBlocks map[string]bool
|
||||
AfterSensitiveBlocks map[string]bool
|
||||
}
|
||||
|
||||
func (blocks *Blocks) GetAllKeys() []string {
|
||||
@ -53,23 +64,30 @@ func (blocks *Blocks) IsSetBlock(key string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func (blocks *Blocks) AddSingleBlock(key string, diff computed.Diff) {
|
||||
func (blocks *Blocks) AddSingleBlock(key string, diff computed.Diff, replace, beforeSensitive, afterSensitive bool) {
|
||||
blocks.SingleBlocks[key] = diff
|
||||
blocks.ReplaceBlocks[key] = replace
|
||||
blocks.BeforeSensitiveBlocks[key] = beforeSensitive
|
||||
blocks.AfterSensitiveBlocks[key] = afterSensitive
|
||||
}
|
||||
|
||||
func (blocks *Blocks) AddListBlock(key string, diff computed.Diff) {
|
||||
blocks.ListBlocks[key] = append(blocks.ListBlocks[key], diff)
|
||||
func (blocks *Blocks) AddAllListBlock(key string, diffs []computed.Diff, replace, beforeSensitive, afterSensitive bool) {
|
||||
blocks.ListBlocks[key] = diffs
|
||||
blocks.ReplaceBlocks[key] = replace
|
||||
blocks.BeforeSensitiveBlocks[key] = beforeSensitive
|
||||
blocks.AfterSensitiveBlocks[key] = afterSensitive
|
||||
}
|
||||
|
||||
func (blocks *Blocks) AddSetBlock(key string, diff computed.Diff) {
|
||||
blocks.SetBlocks[key] = append(blocks.SetBlocks[key], diff)
|
||||
func (blocks *Blocks) AddAllSetBlock(key string, diffs []computed.Diff, replace, beforeSensitive, afterSensitive bool) {
|
||||
blocks.SetBlocks[key] = diffs
|
||||
blocks.ReplaceBlocks[key] = replace
|
||||
blocks.BeforeSensitiveBlocks[key] = beforeSensitive
|
||||
blocks.AfterSensitiveBlocks[key] = afterSensitive
|
||||
}
|
||||
|
||||
func (blocks *Blocks) AddMapBlock(key string, entry string, diff computed.Diff) {
|
||||
m := blocks.MapBlocks[key]
|
||||
if m == nil {
|
||||
m = make(map[string]computed.Diff)
|
||||
}
|
||||
m[entry] = diff
|
||||
blocks.MapBlocks[key] = m
|
||||
func (blocks *Blocks) AddAllMapBlocks(key string, diffs map[string]computed.Diff, replace, beforeSensitive, afterSensitive bool) {
|
||||
blocks.MapBlocks[key] = diffs
|
||||
blocks.ReplaceBlocks[key] = replace
|
||||
blocks.BeforeSensitiveBlocks[key] = beforeSensitive
|
||||
blocks.AfterSensitiveBlocks[key] = afterSensitive
|
||||
}
|
||||
|
@ -58,6 +58,8 @@ func (renderer listRenderer) RenderHuman(diff computed.Diff, indent int, opts co
|
||||
}
|
||||
renderNext = false
|
||||
|
||||
opts := elementOpts
|
||||
|
||||
// If we want to display the context around this change, we want to
|
||||
// render the change immediately before this change in the list, and the
|
||||
// change immediately after in the list, even if both these changes are
|
||||
@ -86,19 +88,26 @@ func (renderer listRenderer) RenderHuman(diff computed.Diff, indent int, opts co
|
||||
// change that happened previously.
|
||||
unchangedElements = nil
|
||||
|
||||
// As we also want to render the element immediately after any
|
||||
// changes, we make a note here to say we should render the next
|
||||
// change whatever it is. But, we only want to render the next
|
||||
// change if the current change isn't a NoOp. If the current change
|
||||
// is a NoOp then it was told to print by the last change and we
|
||||
// don't want to cascade and print all changes from now on.
|
||||
renderNext = element.Action != plans.NoOp
|
||||
if element.Action == plans.NoOp {
|
||||
// If this is a NoOp action then we're going to render it below
|
||||
// so we need to just override the opts we're going to use to
|
||||
// make sure we use the unchanged opts.
|
||||
opts = unchangedElementOpts
|
||||
} else {
|
||||
// As we also want to render the element immediately after any
|
||||
// changes, we make a note here to say we should render the next
|
||||
// change whatever it is. But, we only want to render the next
|
||||
// change if the current change isn't a NoOp. If the current change
|
||||
// is a NoOp then it was told to print by the last change and we
|
||||
// don't want to cascade and print all changes from now on.
|
||||
renderNext = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, warning := range element.WarningsHuman(indent+1, opts) {
|
||||
buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning))
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s,\n", formatIndent(indent+1), colorizeDiffAction(element.Action, opts), element.RenderHuman(indent+1, elementOpts)))
|
||||
buf.WriteString(fmt.Sprintf("%s%s %s,\n", formatIndent(indent+1), colorizeDiffAction(element.Action, opts), element.RenderHuman(indent+1, opts)))
|
||||
}
|
||||
|
||||
// If we were not displaying any context alongside our changes then the
|
||||
|
@ -14,48 +14,63 @@ import (
|
||||
var _ computed.DiffRenderer = (*mapRenderer)(nil)
|
||||
|
||||
func Map(elements map[string]computed.Diff) computed.DiffRenderer {
|
||||
maximumKeyLen := 0
|
||||
for key := range elements {
|
||||
if maximumKeyLen < len(key) {
|
||||
maximumKeyLen = len(key)
|
||||
}
|
||||
}
|
||||
|
||||
return &mapRenderer{
|
||||
elements: elements,
|
||||
maximumKeyLen: maximumKeyLen,
|
||||
elements: elements,
|
||||
}
|
||||
}
|
||||
|
||||
func NestedMap(elements map[string]computed.Diff) computed.DiffRenderer {
|
||||
return &mapRenderer{
|
||||
elements: elements,
|
||||
overrideNullSuffix: true,
|
||||
overrideForcesReplacement: true,
|
||||
}
|
||||
}
|
||||
|
||||
type mapRenderer struct {
|
||||
NoWarningsRenderer
|
||||
|
||||
elements map[string]computed.Diff
|
||||
maximumKeyLen int
|
||||
elements map[string]computed.Diff
|
||||
|
||||
overrideNullSuffix bool
|
||||
overrideForcesReplacement bool
|
||||
}
|
||||
|
||||
func (renderer mapRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string {
|
||||
if len(renderer.elements) == 0 {
|
||||
return fmt.Sprintf("{}%s%s", nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts))
|
||||
}
|
||||
forcesReplacementSelf := diff.Replace && !renderer.overrideForcesReplacement
|
||||
forcesReplacementChildren := diff.Replace && renderer.overrideForcesReplacement
|
||||
|
||||
unchangedElements := 0
|
||||
if len(renderer.elements) == 0 {
|
||||
return fmt.Sprintf("{}%s%s", nullSuffix(diff.Action, opts), forcesReplacement(forcesReplacementSelf, opts))
|
||||
}
|
||||
|
||||
// Sort the map elements by key, so we have a deterministic ordering in
|
||||
// the output.
|
||||
var keys []string
|
||||
|
||||
// We need to make sure the keys are capable of rendering properly.
|
||||
escapedKeys := make(map[string]string)
|
||||
|
||||
maximumKeyLen := 0
|
||||
for key := range renderer.elements {
|
||||
keys = append(keys, key)
|
||||
|
||||
escapedKey := hclEscapeString(key)
|
||||
escapedKeys[key] = escapedKey
|
||||
if maximumKeyLen < len(escapedKey) {
|
||||
maximumKeyLen = len(escapedKey)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
unchangedElements := 0
|
||||
|
||||
elementOpts := opts.Clone()
|
||||
if diff.Action == plans.Delete {
|
||||
elementOpts.OverrideNullSuffix = true
|
||||
}
|
||||
elementOpts.OverrideNullSuffix = diff.Action == plans.Delete || renderer.overrideNullSuffix
|
||||
elementOpts.OverrideForcesReplacement = forcesReplacementChildren
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(fmt.Sprintf("{%s\n", forcesReplacement(diff.Replace, opts)))
|
||||
buf.WriteString(fmt.Sprintf("{%s\n", forcesReplacement(forcesReplacementSelf, opts)))
|
||||
for _, key := range keys {
|
||||
element := renderer.elements[key]
|
||||
|
||||
@ -71,15 +86,11 @@ func (renderer mapRenderer) RenderHuman(diff computed.Diff, indent int, opts com
|
||||
|
||||
// Only show commas between elements for objects.
|
||||
comma := ""
|
||||
if _, ok := element.Renderer.(objectRenderer); ok {
|
||||
if _, ok := element.Renderer.(*objectRenderer); ok {
|
||||
comma = ","
|
||||
}
|
||||
|
||||
// When we add padding for the keys, we want the length to be an
|
||||
// additional 2 characters, as we are going to add quotation marks ("")
|
||||
// around the key when it is rendered.
|
||||
keyLenWithOffset := renderer.maximumKeyLen + 2
|
||||
buf.WriteString(fmt.Sprintf("%s%s %-*q = %s%s\n", formatIndent(indent+1), colorizeDiffAction(element.Action, opts), keyLenWithOffset, key, element.RenderHuman(indent+1, elementOpts), comma))
|
||||
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s%s\n", formatIndent(indent+1), colorizeDiffAction(element.Action, opts), maximumKeyLen, escapedKeys[key], element.RenderHuman(indent+1, elementOpts), comma))
|
||||
}
|
||||
|
||||
if unchangedElements > 0 {
|
||||
|
@ -64,6 +64,17 @@ func (renderer objectRenderer) RenderHuman(diff computed.Diff, indent int, opts
|
||||
for _, key := range keys {
|
||||
attribute := renderer.attributes[key]
|
||||
|
||||
if importantAttribute(key) {
|
||||
importantAttributeOpts := attributeOpts.Clone()
|
||||
importantAttributeOpts.ShowUnchangedChildren = true
|
||||
|
||||
for _, warning := range attribute.WarningsHuman(indent+1, importantAttributeOpts) {
|
||||
buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning))
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("%s%s %-*s = %s\n", formatIndent(indent+1), colorizeDiffAction(attribute.Action, importantAttributeOpts), maximumKeyLen, escapedKeys[key], attribute.RenderHuman(indent+1, importantAttributeOpts)))
|
||||
continue
|
||||
}
|
||||
|
||||
if attribute.Action == plans.NoOp && !opts.ShowUnchangedChildren {
|
||||
// Don't render NoOp operations when we are compact display.
|
||||
unchangedAttributes++
|
||||
|
@ -92,12 +92,12 @@ func (renderer primitiveRenderer) renderStringDiff(diff computed.Diff, indent in
|
||||
// We are creating a single multiline string, so let's split by the new
|
||||
// line character. While we are doing this, we are going to insert our
|
||||
// indents and make sure each line is formatted correctly.
|
||||
lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s ", formatIndent(indent), format.DiffActionSymbol(plans.NoOp))), "\n")
|
||||
lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s ", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp))), "\n")
|
||||
|
||||
// We now just need to do the same for the first entry in lines, because
|
||||
// we split on the new line characters which won't have been at the
|
||||
// beginning of the first line.
|
||||
lines[0] = fmt.Sprintf("%s%s %s", formatIndent(indent), format.DiffActionSymbol(plans.NoOp), lines[0])
|
||||
lines[0] = fmt.Sprintf("%s%s %s", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), lines[0])
|
||||
case plans.Delete:
|
||||
str := evaluatePrimitiveString(renderer.before, opts)
|
||||
|
||||
@ -112,12 +112,12 @@ func (renderer primitiveRenderer) renderStringDiff(diff computed.Diff, indent in
|
||||
// We are creating a single multiline string, so let's split by the new
|
||||
// line character. While we are doing this, we are going to insert our
|
||||
// indents and make sure each line is formatted correctly.
|
||||
lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s ", formatIndent(indent), format.DiffActionSymbol(plans.NoOp))), "\n")
|
||||
lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s ", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp))), "\n")
|
||||
|
||||
// We now just need to do the same for the first entry in lines, because
|
||||
// we split on the new line characters which won't have been at the
|
||||
// beginning of the first line.
|
||||
lines[0] = fmt.Sprintf("%s%s %s", formatIndent(indent), format.DiffActionSymbol(plans.NoOp), lines[0])
|
||||
lines[0] = fmt.Sprintf("%s%s %s", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), lines[0])
|
||||
default:
|
||||
beforeString := evaluatePrimitiveString(renderer.before, opts)
|
||||
afterString := evaluatePrimitiveString(renderer.after, opts)
|
||||
@ -147,16 +147,16 @@ func (renderer primitiveRenderer) renderStringDiff(diff computed.Diff, indent in
|
||||
|
||||
processIndices := func(beforeIx, afterIx int) {
|
||||
if beforeIx < 0 || beforeIx >= len(beforeLines) {
|
||||
lines = append(lines, fmt.Sprintf("%s%s %s", formatIndent(indent), colorizeDiffAction(plans.Create, opts), afterLines[afterIx]))
|
||||
lines = append(lines, fmt.Sprintf("%s%s %s", formatIndent(indent+1), colorizeDiffAction(plans.Create, opts), afterLines[afterIx]))
|
||||
return
|
||||
}
|
||||
|
||||
if afterIx < 0 || afterIx >= len(afterLines) {
|
||||
lines = append(lines, fmt.Sprintf("%s%s %s", formatIndent(indent), colorizeDiffAction(plans.Delete, opts), beforeLines[beforeIx]))
|
||||
lines = append(lines, fmt.Sprintf("%s%s %s", formatIndent(indent+1), colorizeDiffAction(plans.Delete, opts), beforeLines[beforeIx]))
|
||||
return
|
||||
}
|
||||
|
||||
lines = append(lines, fmt.Sprintf("%s%s %s", formatIndent(indent), format.DiffActionSymbol(plans.NoOp), beforeLines[beforeIx]))
|
||||
lines = append(lines, fmt.Sprintf("%s%s %s", formatIndent(indent+1), format.DiffActionSymbol(plans.NoOp), beforeLines[beforeIx]))
|
||||
}
|
||||
isObjType := func(_ string) bool {
|
||||
return false
|
||||
@ -167,17 +167,21 @@ func (renderer primitiveRenderer) renderStringDiff(diff computed.Diff, indent in
|
||||
|
||||
// We return early if we find non-multiline strings or JSON strings, so we
|
||||
// know here that we just render the lines slice properly.
|
||||
return fmt.Sprintf("<<-EOT%s\n%s\n%sEOT%s",
|
||||
return fmt.Sprintf("<<-EOT%s\n%s\n%s%s EOT%s",
|
||||
forcesReplacement(diff.Replace, opts),
|
||||
strings.Join(lines, "\n"),
|
||||
formatIndent(indent),
|
||||
format.DiffActionSymbol(plans.NoOp),
|
||||
nullSuffix(diff.Action, opts))
|
||||
}
|
||||
|
||||
func (renderer primitiveRenderer) renderStringDiffAsJson(diff computed.Diff, indent int, opts computed.RenderHumanOpts, before evaluatedString, after evaluatedString) string {
|
||||
jsonDiff := RendererJsonOpts().Transform(before.Json, after.Json)
|
||||
|
||||
action := diff.Action
|
||||
|
||||
var whitespace, replace string
|
||||
jsonOpts := opts.Clone()
|
||||
if jsonDiff.Action == plans.NoOp && diff.Action == plans.Update {
|
||||
// Then this means we are rendering a whitespace only change. The JSON
|
||||
// differ will have ignored the whitespace changes so that makes the
|
||||
@ -188,16 +192,28 @@ func (renderer primitiveRenderer) renderStringDiffAsJson(diff computed.Diff, ind
|
||||
} else {
|
||||
whitespace = " # whitespace changes"
|
||||
}
|
||||
|
||||
// Because we'd be showing no changes otherwise:
|
||||
jsonOpts.ShowUnchangedChildren = true
|
||||
|
||||
// Whitespace changes should not appear as if edited.
|
||||
action = plans.NoOp
|
||||
} else {
|
||||
// We only show the replace suffix if we didn't print something out
|
||||
// about whitespace changes.
|
||||
replace = forcesReplacement(diff.Replace, opts)
|
||||
}
|
||||
|
||||
renderedJsonDiff := jsonDiff.RenderHuman(indent, opts)
|
||||
renderedJsonDiff := jsonDiff.RenderHuman(indent+1, jsonOpts)
|
||||
|
||||
if diff.Action == plans.Create || diff.Action == plans.Delete {
|
||||
// We don't display the '+' or '-' symbols on the JSON diffs, we should
|
||||
// still display the '~' for an update action though.
|
||||
action = plans.NoOp
|
||||
}
|
||||
|
||||
if strings.Contains(renderedJsonDiff, "\n") {
|
||||
return fmt.Sprintf("jsonencode(%s\n%s%s %s%s\n%s)", whitespace, formatIndent(indent), colorizeDiffAction(diff.Action, opts), renderedJsonDiff, replace, formatIndent(indent))
|
||||
return fmt.Sprintf("jsonencode(%s\n%s%s %s%s\n%s)", whitespace, formatIndent(indent+1), colorizeDiffAction(action, opts), renderedJsonDiff, replace, formatIndent(indent+1))
|
||||
}
|
||||
return fmt.Sprintf("jsonencode(%s)%s%s", renderedJsonDiff, whitespace, replace)
|
||||
}
|
||||
|
@ -82,9 +82,9 @@ func TestRenderers_Human(t *testing.T) {
|
||||
},
|
||||
expected: `
|
||||
<<-EOT
|
||||
hello
|
||||
world
|
||||
EOT
|
||||
hello
|
||||
world
|
||||
EOT
|
||||
`,
|
||||
},
|
||||
"primitive_multiline_string_delete": {
|
||||
@ -94,9 +94,9 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
<<-EOT
|
||||
hello
|
||||
world
|
||||
EOT -> null
|
||||
hello
|
||||
world
|
||||
EOT -> null
|
||||
`,
|
||||
},
|
||||
"primitive_multiline_string_update": {
|
||||
@ -106,11 +106,11 @@ EOT -> null
|
||||
},
|
||||
expected: `
|
||||
<<-EOT
|
||||
hello
|
||||
- old
|
||||
+ new
|
||||
world
|
||||
EOT
|
||||
hello
|
||||
- old
|
||||
+ new
|
||||
world
|
||||
EOT
|
||||
`,
|
||||
},
|
||||
"primitive_json_string_create": {
|
||||
@ -120,11 +120,11 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
jsonencode(
|
||||
+ {
|
||||
+ key_one = "value_one"
|
||||
+ key_two = "value_two"
|
||||
}
|
||||
)
|
||||
{
|
||||
+ key_one = "value_one"
|
||||
+ key_two = "value_two"
|
||||
}
|
||||
)
|
||||
`,
|
||||
},
|
||||
"primitive_json_string_delete": {
|
||||
@ -134,11 +134,11 @@ jsonencode(
|
||||
},
|
||||
expected: `
|
||||
jsonencode(
|
||||
- {
|
||||
- key_one = "value_one"
|
||||
- key_two = "value_two"
|
||||
} -> null
|
||||
)
|
||||
{
|
||||
- key_one = "value_one"
|
||||
- key_two = "value_two"
|
||||
} -> null
|
||||
)
|
||||
`,
|
||||
},
|
||||
"primitive_json_string_update": {
|
||||
@ -148,11 +148,11 @@ jsonencode(
|
||||
},
|
||||
expected: `
|
||||
jsonencode(
|
||||
~ {
|
||||
+ key_three = "value_three"
|
||||
# (2 unchanged attributes hidden)
|
||||
}
|
||||
)
|
||||
~ {
|
||||
+ key_three = "value_three"
|
||||
# (2 unchanged attributes hidden)
|
||||
}
|
||||
)
|
||||
`,
|
||||
},
|
||||
"primitive_fake_json_string_update": {
|
||||
@ -170,14 +170,14 @@ jsonencode(
|
||||
},
|
||||
expected: `
|
||||
<<-EOT
|
||||
hello
|
||||
world
|
||||
EOT -> jsonencode(
|
||||
+ {
|
||||
+ key_one = "value_one"
|
||||
+ key_two = "value_two"
|
||||
}
|
||||
)
|
||||
hello
|
||||
world
|
||||
EOT -> jsonencode(
|
||||
{
|
||||
+ key_one = "value_one"
|
||||
+ key_two = "value_two"
|
||||
}
|
||||
)
|
||||
`,
|
||||
},
|
||||
"primitive_json_to_multiline_update": {
|
||||
@ -187,14 +187,14 @@ EOT -> jsonencode(
|
||||
},
|
||||
expected: `
|
||||
jsonencode(
|
||||
- {
|
||||
- key_one = "value_one"
|
||||
- key_two = "value_two"
|
||||
}
|
||||
) -> <<-EOT
|
||||
hello
|
||||
world
|
||||
EOT
|
||||
{
|
||||
- key_one = "value_one"
|
||||
- key_two = "value_two"
|
||||
}
|
||||
) -> <<-EOT
|
||||
hello
|
||||
world
|
||||
EOT
|
||||
`,
|
||||
},
|
||||
"primitive_json_to_string_update": {
|
||||
@ -204,11 +204,11 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
jsonencode(
|
||||
- {
|
||||
- key_one = "value_one"
|
||||
- key_two = "value_two"
|
||||
}
|
||||
) -> "hello world"
|
||||
{
|
||||
- key_one = "value_one"
|
||||
- key_two = "value_two"
|
||||
}
|
||||
) -> "hello world"
|
||||
`,
|
||||
},
|
||||
"primitive_string_to_json_update": {
|
||||
@ -218,11 +218,11 @@ jsonencode(
|
||||
},
|
||||
expected: `
|
||||
"hello world" -> jsonencode(
|
||||
+ {
|
||||
+ key_one = "value_one"
|
||||
+ key_two = "value_two"
|
||||
}
|
||||
)
|
||||
{
|
||||
+ key_one = "value_one"
|
||||
+ key_two = "value_two"
|
||||
}
|
||||
)
|
||||
`,
|
||||
},
|
||||
"primitive_multi_to_single_update": {
|
||||
@ -232,10 +232,10 @@ jsonencode(
|
||||
},
|
||||
expected: `
|
||||
<<-EOT
|
||||
- hello
|
||||
- world
|
||||
+ hello world
|
||||
EOT
|
||||
- hello
|
||||
- world
|
||||
+ hello world
|
||||
EOT
|
||||
`,
|
||||
},
|
||||
"primitive_single_to_multi_update": {
|
||||
@ -245,10 +245,10 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
<<-EOT
|
||||
- hello world
|
||||
+ hello
|
||||
+ world
|
||||
EOT
|
||||
- hello world
|
||||
+ hello
|
||||
+ world
|
||||
EOT
|
||||
`,
|
||||
},
|
||||
"sensitive_update": {
|
||||
@ -259,7 +259,7 @@ EOT
|
||||
}, true, true),
|
||||
Action: plans.Update,
|
||||
},
|
||||
expected: "(sensitive)",
|
||||
expected: "(sensitive value)",
|
||||
},
|
||||
"sensitive_update_replace": {
|
||||
diff: computed.Diff{
|
||||
@ -271,7 +271,7 @@ EOT
|
||||
Action: plans.Update,
|
||||
Replace: true,
|
||||
},
|
||||
expected: "(sensitive) # forces replacement",
|
||||
expected: "(sensitive value) # forces replacement",
|
||||
},
|
||||
"computed_create": {
|
||||
diff: computed.Diff{
|
||||
@ -465,7 +465,7 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
+ attribute_one = (sensitive)
|
||||
+ attribute_one = (sensitive value)
|
||||
}
|
||||
`,
|
||||
},
|
||||
@ -484,7 +484,7 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
~ attribute_one = (sensitive)
|
||||
~ attribute_one = (sensitive value)
|
||||
}
|
||||
`,
|
||||
},
|
||||
@ -503,7 +503,7 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
- attribute_one = (sensitive)
|
||||
- attribute_one = (sensitive value)
|
||||
}
|
||||
`,
|
||||
},
|
||||
@ -720,7 +720,7 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
+ "element_one" = (sensitive)
|
||||
+ "element_one" = (sensitive value)
|
||||
}
|
||||
`,
|
||||
},
|
||||
@ -739,7 +739,7 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
~ "element_one" = (sensitive)
|
||||
~ "element_one" = (sensitive value)
|
||||
}
|
||||
`,
|
||||
},
|
||||
@ -760,7 +760,7 @@ EOT
|
||||
{
|
||||
# Warning: this attribute value will no longer be marked as sensitive
|
||||
# after applying this change. The value is unchanged.
|
||||
~ "element_one" = (sensitive)
|
||||
~ "element_one" = (sensitive value)
|
||||
}
|
||||
`,
|
||||
},
|
||||
@ -779,7 +779,7 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
- "element_one" = (sensitive) -> null
|
||||
- "element_one" = (sensitive value) -> null
|
||||
}
|
||||
`,
|
||||
},
|
||||
@ -1034,7 +1034,7 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
[
|
||||
+ (sensitive),
|
||||
+ (sensitive value),
|
||||
]
|
||||
`,
|
||||
},
|
||||
@ -1053,7 +1053,7 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
[
|
||||
- (sensitive),
|
||||
- (sensitive value),
|
||||
]
|
||||
`,
|
||||
},
|
||||
@ -1072,7 +1072,7 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
[
|
||||
~ (sensitive),
|
||||
~ (sensitive value),
|
||||
]
|
||||
`,
|
||||
},
|
||||
@ -1093,7 +1093,7 @@ EOT
|
||||
[
|
||||
# Warning: this attribute value will be marked as sensitive and will not
|
||||
# display in UI output after applying this change. The value is unchanged.
|
||||
~ (sensitive),
|
||||
~ (sensitive value),
|
||||
]
|
||||
`,
|
||||
},
|
||||
@ -1336,7 +1336,7 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
[
|
||||
+ (sensitive),
|
||||
+ (sensitive value),
|
||||
]
|
||||
`,
|
||||
},
|
||||
@ -1355,7 +1355,7 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
[
|
||||
- (sensitive),
|
||||
- (sensitive value),
|
||||
]
|
||||
`,
|
||||
},
|
||||
@ -1374,7 +1374,7 @@ EOT
|
||||
},
|
||||
expected: `
|
||||
[
|
||||
~ (sensitive),
|
||||
~ (sensitive value),
|
||||
]
|
||||
`,
|
||||
},
|
||||
@ -1395,7 +1395,7 @@ EOT
|
||||
[
|
||||
# Warning: this attribute value will be marked as sensitive and will not
|
||||
# display in UI output after applying this change.
|
||||
~ (sensitive),
|
||||
~ (sensitive value),
|
||||
]
|
||||
`,
|
||||
},
|
||||
@ -1439,9 +1439,7 @@ EOT
|
||||
Renderer: Block(nil, Blocks{}),
|
||||
Action: plans.Create,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
}`,
|
||||
expected: "{}",
|
||||
},
|
||||
"create_populated_block": {
|
||||
diff: computed.Diff{
|
||||
@ -1586,6 +1584,7 @@ EOT
|
||||
+ nested_block_two {
|
||||
+ string = "two"
|
||||
}
|
||||
|
||||
# (1 unchanged block hidden)
|
||||
}`,
|
||||
},
|
||||
@ -1855,9 +1854,7 @@ EOT
|
||||
Renderer: Block(nil, Blocks{}),
|
||||
Action: plans.Delete,
|
||||
},
|
||||
expected: `
|
||||
{
|
||||
}`,
|
||||
expected: "{}",
|
||||
},
|
||||
"block_escapes_keys": {
|
||||
diff: computed.Diff{
|
||||
@ -1952,6 +1949,7 @@ EOT
|
||||
{
|
||||
id = "root"
|
||||
# (1 unchanged attribute hidden)
|
||||
|
||||
# (2 unchanged blocks hidden)
|
||||
}`,
|
||||
},
|
||||
@ -1998,7 +1996,7 @@ EOT
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
||||
opts := tc.opts.Clone()
|
||||
opts.Colorize = colorize
|
||||
opts.Colorize = &colorize
|
||||
|
||||
expected := strings.TrimSpace(tc.expected)
|
||||
actual := tc.diff.RenderHuman(0, opts)
|
||||
|
@ -25,7 +25,7 @@ type sensitiveRenderer struct {
|
||||
}
|
||||
|
||||
func (renderer sensitiveRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string {
|
||||
return fmt.Sprintf("(sensitive)%s%s", nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts))
|
||||
return fmt.Sprintf("(sensitive value)%s%s", nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts))
|
||||
}
|
||||
|
||||
func (renderer sensitiveRenderer) WarningsHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) []string {
|
||||
|
@ -37,15 +37,9 @@ func (renderer sensitiveBlockRenderer) WarningsHuman(diff computed.Diff, indent
|
||||
return []string{}
|
||||
}
|
||||
|
||||
var warning string
|
||||
if renderer.beforeSensitive {
|
||||
warning = opts.Colorize.Color(fmt.Sprintf(" # [yellow]Warning[reset]: this block will no longer be marked as sensitive\n%s # after applying this change.", formatIndent(indent)))
|
||||
return []string{opts.Colorize.Color(fmt.Sprintf(" # [yellow]Warning[reset]: this block will no longer be marked as sensitive\n%s # after applying this change.", formatIndent(indent)))}
|
||||
} else {
|
||||
warning = opts.Colorize.Color(fmt.Sprintf(" # [yellow]Warning[reset]: this block will be marked as sensitive and will not\n%s # display in UI output after applying this change.", formatIndent(indent)))
|
||||
return []string{opts.Colorize.Color(fmt.Sprintf(" # [yellow]Warning[reset]: this block will be marked as sensitive and will not\n%s # display in UI output after applying this change.", formatIndent(indent)))}
|
||||
}
|
||||
|
||||
if renderer.inner.Action == plans.NoOp {
|
||||
return []string{fmt.Sprintf("%s The value is unchanged.", warning)}
|
||||
}
|
||||
return []string{warning}
|
||||
}
|
||||
|
@ -55,15 +55,27 @@ func unchanged(keyword string, count int, opts computed.RenderHumanOpts) string
|
||||
return opts.Colorize.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %ss hidden)[reset]", count, keyword))
|
||||
}
|
||||
|
||||
// ensureValidAttributeName checks if `name` contains any HCL syntax and returns
|
||||
// it surrounded by quotation marks if it does.
|
||||
// ensureValidAttributeName checks if `name` contains any HCL syntax and calls
|
||||
// and returns hclEscapeString.
|
||||
func ensureValidAttributeName(name string) string {
|
||||
if !hclsyntax.ValidIdentifier(name) {
|
||||
return fmt.Sprintf("%q", name)
|
||||
return hclEscapeString(name)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// hclEscapeString formats the input string into a format that is safe for
|
||||
// rendering within HCL.
|
||||
//
|
||||
// Note, this function doesn't actually do a very good job of this currently. We
|
||||
// need to expose some internal functions from HCL in a future version and call
|
||||
// them from here. For now, just use "%q" formatting.
|
||||
func hclEscapeString(str string) string {
|
||||
// TODO: Replace this with more complete HCL logic instead of the simple
|
||||
// go workaround.
|
||||
return fmt.Sprintf("%q", str)
|
||||
}
|
||||
|
||||
func colorizeDiffAction(action plans.Action, opts computed.RenderHumanOpts) string {
|
||||
return opts.Colorize.Color(format.DiffActionSymbol(action))
|
||||
}
|
||||
|
83
internal/command/jsonformat/diff.go
Normal file
83
internal/command/jsonformat/diff.go
Normal file
@ -0,0 +1,83 @@
|
||||
package jsonformat
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/differ"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonplan"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
func precomputeDiffs(plan Plan) diffs {
|
||||
diffs := diffs{
|
||||
outputs: make(map[string]computed.Diff),
|
||||
}
|
||||
|
||||
for _, drift := range plan.ResourceDrift {
|
||||
schema := plan.ProviderSchemas[drift.ProviderName].ResourceSchemas[drift.Type]
|
||||
diffs.drift = append(diffs.drift, diff{
|
||||
change: drift,
|
||||
diff: differ.FromJsonChange(drift.Change).ComputeDiffForBlock(schema.Block),
|
||||
})
|
||||
}
|
||||
|
||||
for _, change := range plan.ResourceChanges {
|
||||
schema := plan.ProviderSchemas[change.ProviderName].ResourceSchemas[change.Type]
|
||||
diffs.changes = append(diffs.changes, diff{
|
||||
change: change,
|
||||
diff: differ.FromJsonChange(change.Change).ComputeDiffForBlock(schema.Block),
|
||||
})
|
||||
}
|
||||
|
||||
for key, output := range plan.OutputChanges {
|
||||
diffs.outputs[key] = differ.FromJsonChange(output).ComputeDiffForOutput()
|
||||
}
|
||||
|
||||
less := func(drs []diff) func(i, j int) bool {
|
||||
return func(i, j int) bool {
|
||||
iA := drs[i].change.Address
|
||||
jA := drs[j].change.Address
|
||||
if iA == jA {
|
||||
return drs[i].change.Deposed < drs[j].change.Deposed
|
||||
}
|
||||
return iA < jA
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(diffs.drift, less(diffs.drift))
|
||||
sort.Slice(diffs.changes, less(diffs.changes))
|
||||
|
||||
return diffs
|
||||
}
|
||||
|
||||
type diffs struct {
|
||||
drift []diff
|
||||
changes []diff
|
||||
outputs map[string]computed.Diff
|
||||
}
|
||||
|
||||
func (d diffs) Empty() bool {
|
||||
for _, change := range d.changes {
|
||||
if change.diff.Action != plans.NoOp || change.Moved() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, output := range d.outputs {
|
||||
if output.Action != plans.NoOp {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type diff struct {
|
||||
change jsonplan.ResourceChange
|
||||
diff computed.Diff
|
||||
}
|
||||
|
||||
func (d diff) Moved() bool {
|
||||
return len(d.change.PreviousAddress) > 0 && d.change.PreviousAddress != d.change.Address
|
||||
}
|
@ -24,6 +24,20 @@ func (change Change) ComputeDiffForBlock(block *jsonprovider.Block) computed.Dif
|
||||
attributes := make(map[string]computed.Diff)
|
||||
for key, attr := range block.Attributes {
|
||||
childValue := blockValue.getChild(key)
|
||||
|
||||
// Empty strings in blocks should be considered null for legacy reasons.
|
||||
// The SDK doesn't support null strings yet, so we work around this now.
|
||||
if before, ok := childValue.Before.(string); ok && len(before) == 0 {
|
||||
childValue.Before = nil
|
||||
}
|
||||
if after, ok := childValue.After.(string); ok && len(after) == 0 {
|
||||
childValue.After = nil
|
||||
}
|
||||
|
||||
// Always treat changes to blocks as implicit.
|
||||
childValue.BeforeExplicit = false
|
||||
childValue.AfterExplicit = false
|
||||
|
||||
childChange := childValue.ComputeDiffForAttribute(attr)
|
||||
if childChange.Action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
|
||||
// Don't record nil values at all in blocks.
|
||||
@ -35,53 +49,54 @@ func (change Change) ComputeDiffForBlock(block *jsonprovider.Block) computed.Dif
|
||||
}
|
||||
|
||||
blocks := renderers.Blocks{
|
||||
SingleBlocks: make(map[string]computed.Diff),
|
||||
ListBlocks: make(map[string][]computed.Diff),
|
||||
SetBlocks: make(map[string][]computed.Diff),
|
||||
MapBlocks: make(map[string]map[string]computed.Diff),
|
||||
ReplaceBlocks: make(map[string]bool),
|
||||
BeforeSensitiveBlocks: make(map[string]bool),
|
||||
AfterSensitiveBlocks: make(map[string]bool),
|
||||
SingleBlocks: make(map[string]computed.Diff),
|
||||
ListBlocks: make(map[string][]computed.Diff),
|
||||
SetBlocks: make(map[string][]computed.Diff),
|
||||
MapBlocks: make(map[string]map[string]computed.Diff),
|
||||
}
|
||||
|
||||
for key, blockType := range block.BlockTypes {
|
||||
childValue := blockValue.getChild(key)
|
||||
|
||||
beforeSensitive := childValue.isBeforeSensitive()
|
||||
afterSensitive := childValue.isAfterSensitive()
|
||||
forcesReplacement := childValue.ReplacePaths.ForcesReplacement()
|
||||
|
||||
switch NestingMode(blockType.NestingMode) {
|
||||
case nestingModeSet:
|
||||
diffs, action := childValue.computeBlockDiffsAsSet(blockType.Block)
|
||||
if action == plans.NoOp && childValue.Before == childValue.After {
|
||||
if action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
|
||||
// Don't record nil values in blocks.
|
||||
continue
|
||||
}
|
||||
for _, diff := range diffs {
|
||||
blocks.AddSetBlock(key, diff)
|
||||
}
|
||||
blocks.AddAllSetBlock(key, diffs, forcesReplacement, beforeSensitive, afterSensitive)
|
||||
current = collections.CompareActions(current, action)
|
||||
case nestingModeList:
|
||||
diffs, action := childValue.computeBlockDiffsAsList(blockType.Block)
|
||||
if action == plans.NoOp && childValue.Before == childValue.After {
|
||||
if action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
|
||||
// Don't record nil values in blocks.
|
||||
continue
|
||||
}
|
||||
for _, diff := range diffs {
|
||||
blocks.AddListBlock(key, diff)
|
||||
}
|
||||
blocks.AddAllListBlock(key, diffs, forcesReplacement, beforeSensitive, afterSensitive)
|
||||
current = collections.CompareActions(current, action)
|
||||
case nestingModeMap:
|
||||
diffs, action := childValue.computeBlockDiffsAsMap(blockType.Block)
|
||||
if action == plans.NoOp && childValue.Before == childValue.After {
|
||||
if action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
|
||||
// Don't record nil values in blocks.
|
||||
continue
|
||||
}
|
||||
for dKey, diff := range diffs {
|
||||
blocks.AddMapBlock(key, dKey, diff)
|
||||
}
|
||||
blocks.AddAllMapBlocks(key, diffs, forcesReplacement, beforeSensitive, afterSensitive)
|
||||
current = collections.CompareActions(current, action)
|
||||
case nestingModeSingle, nestingModeGroup:
|
||||
diff := childValue.ComputeDiffForBlock(blockType.Block)
|
||||
if diff.Action == plans.NoOp && childValue.Before == childValue.After {
|
||||
if diff.Action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
|
||||
// Don't record nil values in blocks.
|
||||
continue
|
||||
}
|
||||
blocks.AddSingleBlock(key, diff)
|
||||
blocks.AddSingleBlock(key, diff, forcesReplacement, beforeSensitive, afterSensitive)
|
||||
current = collections.CompareActions(current, diff.Action)
|
||||
default:
|
||||
panic("unrecognized nesting mode: " + blockType.NestingMode)
|
||||
|
@ -82,9 +82,9 @@ type Change struct {
|
||||
ReplacePaths replace.ForcesReplacement
|
||||
}
|
||||
|
||||
// ValueFromJsonChange unmarshals the raw []byte values in the jsonplan.Change
|
||||
// FromJsonChange unmarshals the raw []byte values in the jsonplan.Change
|
||||
// structs into generic interface{} types that can be reasoned about.
|
||||
func ValueFromJsonChange(change jsonplan.Change) Change {
|
||||
func FromJsonChange(change jsonplan.Change) Change {
|
||||
return Change{
|
||||
Before: unmarshalGeneric(change.Before),
|
||||
After: unmarshalGeneric(change.After),
|
||||
|
@ -156,8 +156,8 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
"attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false),
|
||||
}, plans.Delete, false), plans.Update, false),
|
||||
validateNestedObject: renderers.ValidateUnknown(renderers.ValidateNestedObject(map[string]renderers.ValidateDiffFunction{
|
||||
"attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false),
|
||||
}, plans.Delete, false), plans.Update, false),
|
||||
"attribute_one": renderers.ValidateUnknown(renderers.ValidatePrimitive("old", nil, plans.Delete, false), plans.Update, false),
|
||||
}, plans.Update, false), plans.Update, false),
|
||||
validateSetDiffs: &SetDiff{
|
||||
Before: SetDiffEntry{
|
||||
ObjectDiff: map[string]renderers.ValidateDiffFunction{
|
||||
@ -451,20 +451,6 @@ func TestValue_ObjectAttributes(t *testing.T) {
|
||||
},
|
||||
validateAction: plans.Update,
|
||||
validateReplace: false,
|
||||
validateSetDiffs: &SetDiff{
|
||||
Before: SetDiffEntry{
|
||||
ObjectDiff: nil,
|
||||
Action: plans.Delete,
|
||||
Replace: false,
|
||||
},
|
||||
After: SetDiffEntry{
|
||||
ObjectDiff: map[string]renderers.ValidateDiffFunction{
|
||||
"attribute_one": renderers.ValidateUnknown(nil, plans.Create, false),
|
||||
},
|
||||
Action: plans.Create,
|
||||
Replace: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
"update_computed_attribute": {
|
||||
input: Change{
|
||||
|
@ -26,7 +26,7 @@ func (change Change) computeAttributeDiffAsNestedMap(attributes map[string]*json
|
||||
NestingMode: "single",
|
||||
})
|
||||
})
|
||||
return computed.NewDiff(renderers.Map(elements), current, change.ReplacePaths.ForcesReplacement())
|
||||
return computed.NewDiff(renderers.NestedMap(elements), current, change.ReplacePaths.ForcesReplacement())
|
||||
}
|
||||
|
||||
func (change Change) computeBlockDiffsAsMap(block *jsonprovider.Block) (map[string]computed.Diff, plans.Action) {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
)
|
||||
|
||||
type CreateSensitiveRenderer func(computed.Diff, bool, bool) computed.DiffRenderer
|
||||
@ -56,7 +57,14 @@ func (change Change) checkForSensitive(create CreateSensitiveRenderer, computedD
|
||||
|
||||
inner := computedDiff(value)
|
||||
|
||||
return computed.NewDiff(create(inner, beforeSensitive, afterSensitive), inner.Action, change.ReplacePaths.ForcesReplacement()), true
|
||||
action := inner.Action
|
||||
if action == plans.NoOp && beforeSensitive != afterSensitive {
|
||||
// Let's override this, since it means the sensitive status has changed
|
||||
// rather than the actual content of the value.
|
||||
action = plans.Update
|
||||
}
|
||||
|
||||
return computed.NewDiff(create(inner, beforeSensitive, afterSensitive), action, change.ReplacePaths.ForcesReplacement()), true
|
||||
}
|
||||
|
||||
func (change Change) isBeforeSensitive() bool {
|
||||
|
@ -38,10 +38,6 @@ func (change Change) computeAttributeDiffAsNestedSet(attributes map[string]*json
|
||||
}
|
||||
|
||||
func (change Change) computeBlockDiffsAsSet(block *jsonprovider.Block) ([]computed.Diff, plans.Action) {
|
||||
// TODO(liamcervante): In the upcoming block PR, block sets should override
|
||||
// the forces replacement setting. We can't do that here, so make sure it
|
||||
// gets carried over into the diff renderer logic when set and list blocks
|
||||
// are given separate behaviour.
|
||||
var elements []computed.Diff
|
||||
current := change.getDefaultActionForIteration()
|
||||
change.processSet(func(value Change) {
|
||||
@ -71,7 +67,7 @@ func (change Change) processSet(process func(value Change)) {
|
||||
}
|
||||
|
||||
child := sliceValue.getChild(ix, jx)
|
||||
if reflect.DeepEqual(child.Before, child.After) && child.isBeforeSensitive() == child.isAfterSensitive() && child.Unknown == nil {
|
||||
if reflect.DeepEqual(child.Before, child.After) && child.isBeforeSensitive() == child.isAfterSensitive() && !child.isUnknown() {
|
||||
matched = true
|
||||
foundInBefore[ix] = jx
|
||||
foundInAfter[jx] = ix
|
||||
|
@ -11,23 +11,39 @@ import (
|
||||
)
|
||||
|
||||
func (change Change) checkForUnknownType(ctype cty.Type) (computed.Diff, bool) {
|
||||
return change.checkForUnknown(func(value Change) computed.Diff {
|
||||
return change.checkForUnknown(false, func(value Change) computed.Diff {
|
||||
return value.computeDiffForType(ctype)
|
||||
})
|
||||
}
|
||||
func (change Change) checkForUnknownNestedAttribute(attribute *jsonprovider.NestedType) (computed.Diff, bool) {
|
||||
return change.checkForUnknown(func(value Change) computed.Diff {
|
||||
|
||||
// We want our child attributes to show up as computed instead of deleted.
|
||||
// Let's populate that here.
|
||||
childUnknown := make(map[string]interface{})
|
||||
for key := range attribute.Attributes {
|
||||
childUnknown[key] = true
|
||||
}
|
||||
|
||||
return change.checkForUnknown(childUnknown, func(value Change) computed.Diff {
|
||||
return value.computeDiffForNestedAttribute(attribute)
|
||||
})
|
||||
}
|
||||
|
||||
func (change Change) checkForUnknownBlock(block *jsonprovider.Block) (computed.Diff, bool) {
|
||||
return change.checkForUnknown(func(value Change) computed.Diff {
|
||||
|
||||
// We want our child attributes to show up as computed instead of deleted.
|
||||
// Let's populate that here.
|
||||
childUnknown := make(map[string]interface{})
|
||||
for key := range block.Attributes {
|
||||
childUnknown[key] = true
|
||||
}
|
||||
|
||||
return change.checkForUnknown(childUnknown, func(value Change) computed.Diff {
|
||||
return value.ComputeDiffForBlock(block)
|
||||
})
|
||||
}
|
||||
|
||||
func (change Change) checkForUnknown(computeDiff func(value Change) computed.Diff) (computed.Diff, bool) {
|
||||
func (change Change) checkForUnknown(childUnknown interface{}, computeDiff func(value Change) computed.Diff) (computed.Diff, bool) {
|
||||
unknown := change.isUnknown()
|
||||
|
||||
if !unknown {
|
||||
@ -50,6 +66,7 @@ func (change Change) checkForUnknown(computeDiff func(value Change) computed.Dif
|
||||
beforeValue := Change{
|
||||
Before: change.Before,
|
||||
BeforeSensitive: change.BeforeSensitive,
|
||||
Unknown: childUnknown,
|
||||
}
|
||||
return change.asDiff(renderers.Unknown(computeDiff(beforeValue))), true
|
||||
}
|
||||
|
@ -1,29 +1,461 @@
|
||||
package jsonformat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/colorstring"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonplan"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonprovider"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/terminal"
|
||||
)
|
||||
|
||||
type RendererOpt int
|
||||
|
||||
const (
|
||||
detectedDrift string = "drift"
|
||||
proposedChange string = "change"
|
||||
|
||||
Errored RendererOpt = iota
|
||||
CanNotApply
|
||||
)
|
||||
|
||||
type Plan struct {
|
||||
OutputChanges map[string]jsonplan.Change `json:"output_changes"`
|
||||
ResourceChanges []jsonplan.ResourceChange `json:"resource_changes"`
|
||||
ResourceDrift []jsonplan.ResourceChange `json:"resource_drift"`
|
||||
ProviderSchemas map[string]*jsonprovider.Provider `json:"provider_schemas"`
|
||||
PlanFormatVersion string `json:"plan_format_version"`
|
||||
OutputChanges map[string]jsonplan.Change `json:"output_changes"`
|
||||
ResourceChanges []jsonplan.ResourceChange `json:"resource_changes"`
|
||||
ResourceDrift []jsonplan.ResourceChange `json:"resource_drift"`
|
||||
|
||||
ProviderFormatVersion string `json:"provider_format_version"`
|
||||
ProviderSchemas map[string]*jsonprovider.Provider `json:"provider_schemas"`
|
||||
}
|
||||
|
||||
type Renderer struct {
|
||||
Streams *terminal.Streams
|
||||
Colorize *colorstring.Colorize
|
||||
|
||||
RunningInAutomation bool
|
||||
}
|
||||
|
||||
func (r Renderer) RenderPlan(plan Plan) {
|
||||
panic("not implemented")
|
||||
func (r Renderer) RenderHumanPlan(plan Plan, mode plans.Mode, opts ...RendererOpt) {
|
||||
// TODO(liamcervante): Tidy up this detection of version differences, we
|
||||
// should only report warnings when the plan is generated using a newer
|
||||
// version then we are executing. We could also look into major vs minor
|
||||
// version differences. This should work for alpha testing in the meantime.
|
||||
if plan.PlanFormatVersion != jsonplan.FormatVersion || plan.ProviderFormatVersion != jsonprovider.FormatVersion {
|
||||
r.Streams.Println(format.WordWrap(
|
||||
r.Colorize.Color("\n[bold][red]Warning:[reset][bold] This plan was generated using a different version of Terraform, the diff presented here maybe missing representations of recent features."),
|
||||
r.Streams.Stdout.Columns()))
|
||||
}
|
||||
|
||||
checkOpts := func(target RendererOpt) bool {
|
||||
for _, opt := range opts {
|
||||
if opt == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
diffs := precomputeDiffs(plan)
|
||||
haveRefreshChanges := r.renderHumanDiffDrift(diffs, mode)
|
||||
|
||||
willPrintResourceChanges := false
|
||||
counts := make(map[plans.Action]int)
|
||||
var changes []diff
|
||||
for _, diff := range diffs.changes {
|
||||
action := jsonplan.UnmarshalActions(diff.change.Change.Actions)
|
||||
if action == plans.NoOp && !diff.Moved() {
|
||||
// Don't show anything for NoOp changes.
|
||||
continue
|
||||
}
|
||||
if action == plans.Delete && diff.change.Mode != "managed" {
|
||||
// Don't render anything for deleted data sources.
|
||||
continue
|
||||
}
|
||||
|
||||
changes = append(changes, diff)
|
||||
willPrintResourceChanges = true
|
||||
|
||||
// Don't count move-only changes
|
||||
if action != plans.NoOp {
|
||||
counts[action]++
|
||||
}
|
||||
}
|
||||
|
||||
if len(changes) == 0 && len(diffs.outputs) == 0 {
|
||||
// If we didn't find any changes to report at all then this is a
|
||||
// "No changes" plan. How we'll present this depends on whether
|
||||
// the plan is "applyable" and, if so, whether it had refresh changes
|
||||
// that we already would've presented above.
|
||||
|
||||
if checkOpts(Errored) {
|
||||
if haveRefreshChanges {
|
||||
r.Streams.Print(format.HorizontalRule(r.Colorize, r.Streams.Stdout.Columns()))
|
||||
r.Streams.Println()
|
||||
}
|
||||
r.Streams.Print(
|
||||
r.Colorize.Color("\n[reset][bold][red]Planning failed.[reset][bold] Terraform encountered an error while generating this plan.[reset]\n\n"),
|
||||
)
|
||||
} else {
|
||||
switch mode {
|
||||
case plans.RefreshOnlyMode:
|
||||
if haveRefreshChanges {
|
||||
// We already generated a sufficient prompt about what will
|
||||
// happen if applying this change above, so we don't need to
|
||||
// say anything more.
|
||||
return
|
||||
}
|
||||
|
||||
r.Streams.Print(r.Colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure still matches the configuration.[reset]\n\n"))
|
||||
r.Streams.Println(format.WordWrap(
|
||||
"Terraform has checked that the real remote objects still match the result of your most recent changes, and found no differences.",
|
||||
r.Streams.Stdout.Columns()))
|
||||
case plans.DestroyMode:
|
||||
if haveRefreshChanges {
|
||||
r.Streams.Print(format.HorizontalRule(r.Colorize, r.Streams.Stdout.Columns()))
|
||||
fmt.Fprintln(r.Streams.Stdout.File)
|
||||
}
|
||||
r.Streams.Print(r.Colorize.Color("\n[reset][bold][green]No changes.[reset][bold] No objects need to be destroyed.[reset]\n\n"))
|
||||
r.Streams.Println(format.WordWrap(
|
||||
"Either you have not created any objects yet or the existing objects were already deleted outside of Terraform.",
|
||||
r.Streams.Stdout.Columns()))
|
||||
default:
|
||||
if haveRefreshChanges {
|
||||
r.Streams.Print(format.HorizontalRule(r.Colorize, r.Streams.Stdout.Columns()))
|
||||
r.Streams.Println("")
|
||||
}
|
||||
r.Streams.Print(
|
||||
r.Colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure matches the configuration.[reset]\n\n"),
|
||||
)
|
||||
|
||||
if haveRefreshChanges {
|
||||
if !checkOpts(CanNotApply) {
|
||||
// In this case, applying this plan will not change any
|
||||
// remote objects but _will_ update the state to match what
|
||||
// we detected during refresh, so we'll reassure the user
|
||||
// about that.
|
||||
r.Streams.Println(format.WordWrap(
|
||||
"Your configuration already matches the changes detected above, so applying this plan will only update the state to include the changes detected above and won't change any real infrastructure.",
|
||||
r.Streams.Stdout.Columns(),
|
||||
))
|
||||
} else {
|
||||
// In this case we detected changes during refresh but this isn't
|
||||
// a planning mode where we consider those to be applyable. The
|
||||
// user must re-run in refresh-only mode in order to update the
|
||||
// state to match the upstream changes.
|
||||
suggestion := "."
|
||||
if !r.RunningInAutomation {
|
||||
// The normal message includes a specific command line to run.
|
||||
suggestion = ":\n terraform apply -refresh-only"
|
||||
}
|
||||
r.Streams.Println(format.WordWrap(
|
||||
"Your configuration already matches the changes detected above. If you'd like to update the Terraform state to match, create and apply a refresh-only plan"+suggestion,
|
||||
r.Streams.Stdout.Columns(),
|
||||
))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If we get down here then we're just in the simple situation where
|
||||
// the plan isn't applyable at all.
|
||||
r.Streams.Println(format.WordWrap(
|
||||
"Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.",
|
||||
r.Streams.Stdout.Columns(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if haveRefreshChanges {
|
||||
r.Streams.Print(format.HorizontalRule(r.Colorize, r.Streams.Stdout.Columns()))
|
||||
r.Streams.Println()
|
||||
}
|
||||
|
||||
if willPrintResourceChanges {
|
||||
r.Streams.Println("\nTerraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:")
|
||||
if counts[plans.Create] > 0 {
|
||||
r.Streams.Println(r.Colorize.Color(actionDescription(plans.Create)))
|
||||
}
|
||||
if counts[plans.Update] > 0 {
|
||||
r.Streams.Println(r.Colorize.Color(actionDescription(plans.Update)))
|
||||
}
|
||||
if counts[plans.Delete] > 0 {
|
||||
r.Streams.Println(r.Colorize.Color(actionDescription(plans.Delete)))
|
||||
}
|
||||
if counts[plans.DeleteThenCreate] > 0 {
|
||||
r.Streams.Println(r.Colorize.Color(actionDescription(plans.DeleteThenCreate)))
|
||||
}
|
||||
if counts[plans.CreateThenDelete] > 0 {
|
||||
r.Streams.Println(r.Colorize.Color(actionDescription(plans.CreateThenDelete)))
|
||||
}
|
||||
if counts[plans.Read] > 0 {
|
||||
r.Streams.Println(r.Colorize.Color(actionDescription(plans.Read)))
|
||||
}
|
||||
}
|
||||
|
||||
if len(changes) > 0 {
|
||||
if checkOpts(Errored) {
|
||||
r.Streams.Printf("\nTerraform planned the following actions, but then encountered a problem:\n\n")
|
||||
} else {
|
||||
r.Streams.Printf("\nTerraform will perform the following actions:\n\n")
|
||||
}
|
||||
|
||||
for _, change := range changes {
|
||||
diff, render := r.renderHumanDiff(change, proposedChange)
|
||||
if render {
|
||||
fmt.Fprintln(r.Streams.Stdout.File)
|
||||
r.Streams.Println(diff)
|
||||
}
|
||||
}
|
||||
|
||||
r.Streams.Printf(
|
||||
r.Colorize.Color("\n[bold]Plan:[reset] %d to add, %d to change, %d to destroy.\n"),
|
||||
counts[plans.Create]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete],
|
||||
counts[plans.Update],
|
||||
counts[plans.Delete]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete])
|
||||
}
|
||||
|
||||
diff := r.renderHumanDiffOutputs(diffs.outputs)
|
||||
if len(diff) > 0 {
|
||||
r.Streams.Print("\nChanges to Outputs:\n")
|
||||
r.Streams.Printf("%s\n", diff)
|
||||
|
||||
if len(counts) == 0 {
|
||||
// If we have output changes but not resource changes then we
|
||||
// won't have output any indication about the changes at all yet,
|
||||
// so we need some extra context about what it would mean to
|
||||
// apply a change that _only_ includes output changes.
|
||||
r.Streams.Println(format.WordWrap(
|
||||
"\nYou can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.",
|
||||
r.Streams.Stdout.Columns()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r Renderer) renderHumanDiffOutputs(outputs map[string]computed.Diff) string {
|
||||
var rendered []string
|
||||
|
||||
var keys []string
|
||||
for key := range outputs {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
output := outputs[key]
|
||||
if output.Action != plans.NoOp {
|
||||
rendered = append(rendered, fmt.Sprintf("%s %s = %s", r.Colorize.Color(format.DiffActionSymbol(output.Action)), key, output.RenderHuman(0, computed.NewRenderHumanOpts(r.Colorize))))
|
||||
}
|
||||
}
|
||||
return strings.Join(rendered, "\n")
|
||||
}
|
||||
|
||||
func (r Renderer) renderHumanDiffDrift(diffs diffs, mode plans.Mode) bool {
|
||||
var drs []diff
|
||||
if mode == plans.RefreshOnlyMode {
|
||||
drs = diffs.drift
|
||||
} else {
|
||||
for _, dr := range diffs.drift {
|
||||
// TODO(liamcervante): Look into if we have to keep filtering resource changes.
|
||||
// For now we still want to remove the moved resources from here as
|
||||
// they will show up in the regular changes.
|
||||
if dr.diff.Action != plans.NoOp {
|
||||
drs = append(drs, dr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(drs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if diffs.Empty() && mode != plans.RefreshOnlyMode {
|
||||
return false
|
||||
}
|
||||
|
||||
r.Streams.Print(r.Colorize.Color("\n[bold][cyan]Note:[reset][bold] Objects have changed outside of Terraform\n"))
|
||||
r.Streams.Println()
|
||||
r.Streams.Print(format.WordWrap(
|
||||
"Terraform detected the following changes made outside of Terraform since the last \"terraform apply\" which may have affected this plan:\n",
|
||||
r.Streams.Stdout.Columns()))
|
||||
|
||||
for _, drift := range drs {
|
||||
diff, render := r.renderHumanDiff(drift, detectedDrift)
|
||||
if render {
|
||||
r.Streams.Println()
|
||||
r.Streams.Println(diff)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (r Renderer) renderHumanDiff(diff diff, cause string) (string, bool) {
|
||||
|
||||
// Internally, our computed diffs can't tell the difference between a
|
||||
// replace action (eg. CreateThenDestroy, DestroyThenCreate) and a simple
|
||||
// update action. So, at the top most level we rely on the action provided
|
||||
// by the plan itself instead of what we compute. Nested attributes and
|
||||
// blocks however don't have the replace type of actions, so we can trust
|
||||
// the computed actions of these.
|
||||
|
||||
action := jsonplan.UnmarshalActions(diff.change.Change.Actions)
|
||||
if action == plans.NoOp && (len(diff.change.PreviousAddress) == 0 || diff.change.PreviousAddress == diff.change.Address) {
|
||||
// Skip resource changes that have nothing interesting to say.
|
||||
return "", false
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(r.Colorize.Color(resourceChangeComment(diff.change, action, cause)))
|
||||
buf.WriteString(fmt.Sprintf("%s %s %s", r.Colorize.Color(format.DiffActionSymbol(action)), resourceChangeHeader(diff.change), diff.diff.RenderHuman(0, computed.NewRenderHumanOpts(r.Colorize))))
|
||||
return buf.String(), true
|
||||
}
|
||||
|
||||
func (r Renderer) RenderLog(message map[string]interface{}) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func resourceChangeComment(resource jsonplan.ResourceChange, action plans.Action, changeCause string) string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
dispAddr := resource.Address
|
||||
if len(resource.Deposed) != 0 {
|
||||
dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, resource.Deposed)
|
||||
}
|
||||
|
||||
switch action {
|
||||
case plans.Create:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be created", dispAddr))
|
||||
case plans.Read:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be read during apply", dispAddr))
|
||||
switch resource.ActionReason {
|
||||
case jsonplan.ResourceInstanceReadBecauseConfigUnknown:
|
||||
buf.WriteString("\n # (config refers to values not yet known)")
|
||||
case jsonplan.ResourceInstanceReadBecauseDependencyPending:
|
||||
buf.WriteString("\n # (depends on a resource or a module with changes pending)")
|
||||
}
|
||||
case plans.Update:
|
||||
switch changeCause {
|
||||
case proposedChange:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be updated in-place", dispAddr))
|
||||
case detectedDrift:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] has changed", dispAddr))
|
||||
default:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] update (unknown reason %s)", dispAddr, changeCause))
|
||||
}
|
||||
case plans.CreateThenDelete, plans.DeleteThenCreate:
|
||||
switch resource.ActionReason {
|
||||
case jsonplan.ResourceInstanceReplaceBecauseTainted:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] is tainted, so must be [bold][red]replaced[reset]", dispAddr))
|
||||
case jsonplan.ResourceInstanceReplaceByRequest:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]replaced[reset], as requested", dispAddr))
|
||||
case jsonplan.ResourceInstanceReplaceByTriggers:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]replaced[reset] due to changes in replace_triggered_by", dispAddr))
|
||||
default:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] must be [bold][red]replaced[reset]", dispAddr))
|
||||
}
|
||||
case plans.Delete:
|
||||
switch changeCause {
|
||||
case proposedChange:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]destroyed[reset]", dispAddr))
|
||||
case detectedDrift:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] has been deleted", dispAddr))
|
||||
default:
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] delete (unknown reason %s)", dispAddr, changeCause))
|
||||
}
|
||||
// We can sometimes give some additional detail about why we're
|
||||
// proposing to delete. We show this as additional notes, rather than
|
||||
// as additional wording in the main action statement, in an attempt
|
||||
// to make the "will be destroyed" message prominent and consistent
|
||||
// in all cases, for easier scanning of this often-risky action.
|
||||
switch resource.ActionReason {
|
||||
case jsonplan.ResourceInstanceDeleteBecauseNoResourceConfig:
|
||||
buf.WriteString(fmt.Sprintf("\n # (because %s.%s is not in configuration)", resource.Type, resource.Name))
|
||||
case jsonplan.ResourceInstanceDeleteBecauseNoMoveTarget:
|
||||
buf.WriteString(fmt.Sprintf("\n # (because %s was moved to %s, which is not in configuration)", resource.PreviousAddress, resource.Address))
|
||||
case jsonplan.ResourceInstanceDeleteBecauseNoModule:
|
||||
// FIXME: Ideally we'd truncate addr.Module to reflect the earliest
|
||||
// step that doesn't exist, so it's clearer which call this refers
|
||||
// to, but we don't have enough information out here in the UI layer
|
||||
// to decide that; only the "expander" in Terraform Core knows
|
||||
// which module instance keys are actually declared.
|
||||
buf.WriteString(fmt.Sprintf("\n # (because %s is not in configuration)", resource.ModuleAddress))
|
||||
case jsonplan.ResourceInstanceDeleteBecauseWrongRepetition:
|
||||
var index interface{}
|
||||
if resource.Index != nil {
|
||||
if err := json.Unmarshal(resource.Index, &index); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// We have some different variations of this one
|
||||
switch index.(type) {
|
||||
case nil:
|
||||
buf.WriteString("\n # (because resource uses count or for_each)")
|
||||
case float64:
|
||||
buf.WriteString("\n # (because resource does not use count)")
|
||||
case string:
|
||||
buf.WriteString("\n # (because resource does not use for_each)")
|
||||
}
|
||||
case jsonplan.ResourceInstanceDeleteBecauseCountIndex:
|
||||
buf.WriteString(fmt.Sprintf("\n # (because index [%s] is out of range for count)", resource.Index))
|
||||
case jsonplan.ResourceInstanceDeleteBecauseEachKey:
|
||||
buf.WriteString(fmt.Sprintf("\n # (because key [%s] is not in for_each map)", resource.Index))
|
||||
}
|
||||
if len(resource.Deposed) != 0 {
|
||||
// Some extra context about this unusual situation.
|
||||
buf.WriteString("\n # (left over from a partially-failed replacement of this instance)")
|
||||
}
|
||||
case plans.NoOp:
|
||||
if len(resource.PreviousAddress) > 0 && resource.PreviousAddress != resource.Address {
|
||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] has moved to [bold]%s[reset]", resource.PreviousAddress, dispAddr))
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
// should never happen, since the above is exhaustive
|
||||
buf.WriteString(fmt.Sprintf("%s has an action the plan renderer doesn't support (this is a bug)", dispAddr))
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
|
||||
if len(resource.PreviousAddress) > 0 && resource.PreviousAddress != resource.Address && action != plans.NoOp {
|
||||
buf.WriteString(fmt.Sprintf(" # [reset](moved from %s)\n", resource.PreviousAddress))
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func resourceChangeHeader(change jsonplan.ResourceChange) string {
|
||||
mode := "resource"
|
||||
if change.Mode != "managed" {
|
||||
mode = "data"
|
||||
}
|
||||
return fmt.Sprintf("%s \"%s\" \"%s\"", mode, change.Type, change.Name)
|
||||
}
|
||||
|
||||
func actionDescription(action plans.Action) string {
|
||||
switch action {
|
||||
case plans.Create:
|
||||
return " [green]+[reset] create"
|
||||
case plans.Delete:
|
||||
return " [red]-[reset] destroy"
|
||||
case plans.Update:
|
||||
return " [yellow]~[reset] update in-place"
|
||||
case plans.CreateThenDelete:
|
||||
return "[green]+[reset]/[red]-[reset] create replacement and then destroy"
|
||||
case plans.DeleteThenCreate:
|
||||
return "[red]-[reset]/[green]+[reset] destroy and then create replacement"
|
||||
case plans.Read:
|
||||
return " [cyan]<=[reset] read (data resources)"
|
||||
default:
|
||||
panic(fmt.Sprintf("unrecognized change type: %s", action.String()))
|
||||
}
|
||||
}
|
||||
|
6900
internal/command/jsonformat/renderer_test.go
Normal file
6900
internal/command/jsonformat/renderer_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
@ -23,7 +24,22 @@ import (
|
||||
// FormatVersion represents the version of the json format and will be
|
||||
// incremented for any change to this format that requires changes to a
|
||||
// consuming parser.
|
||||
const FormatVersion = "1.1"
|
||||
const (
|
||||
FormatVersion = "1.1"
|
||||
|
||||
ResourceInstanceReplaceBecauseCannotUpdate = "replace_because_cannot_update"
|
||||
ResourceInstanceReplaceBecauseTainted = "replace_because_tainted"
|
||||
ResourceInstanceReplaceByRequest = "replace_by_request"
|
||||
ResourceInstanceReplaceByTriggers = "replace_by_triggers"
|
||||
ResourceInstanceDeleteBecauseNoResourceConfig = "delete_because_no_resource_config"
|
||||
ResourceInstanceDeleteBecauseWrongRepetition = "delete_because_wrong_repetition"
|
||||
ResourceInstanceDeleteBecauseCountIndex = "delete_because_count_index"
|
||||
ResourceInstanceDeleteBecauseEachKey = "delete_because_each_key"
|
||||
ResourceInstanceDeleteBecauseNoModule = "delete_because_no_module"
|
||||
ResourceInstanceDeleteBecauseNoMoveTarget = "delete_because_no_move_target"
|
||||
ResourceInstanceReadBecauseConfigUnknown = "read_because_config_unknown"
|
||||
ResourceInstanceReadBecauseDependencyPending = "read_because_dependency_pending"
|
||||
)
|
||||
|
||||
// Plan is the top-level representation of the json format of a plan. It includes
|
||||
// the complete config and current state.
|
||||
@ -156,7 +172,7 @@ func Marshal(
|
||||
}
|
||||
}
|
||||
}
|
||||
output.ResourceDrift, err = output.marshalResourceChanges(driftedResources, schemas)
|
||||
output.ResourceDrift, err = MarshalResourceChanges(driftedResources, schemas)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in marshaling resource drift: %s", err)
|
||||
}
|
||||
@ -168,15 +184,14 @@ func Marshal(
|
||||
|
||||
// output.ResourceChanges
|
||||
if p.Changes != nil {
|
||||
output.ResourceChanges, err = output.marshalResourceChanges(p.Changes.Resources, schemas)
|
||||
output.ResourceChanges, err = MarshalResourceChanges(p.Changes.Resources, schemas)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in marshaling resource changes: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// output.OutputChanges
|
||||
err = output.marshalOutputChanges(p.Changes)
|
||||
if err != nil {
|
||||
if output.OutputChanges, err = MarshalOutputChanges(p.Changes); err != nil {
|
||||
return nil, fmt.Errorf("error in marshaling output changes: %s", err)
|
||||
}
|
||||
|
||||
@ -257,7 +272,13 @@ func (p *plan) marshalPlanVariables(vars map[string]plans.DynamicValue, decls ma
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *plan) marshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schemas *terraform.Schemas) ([]ResourceChange, error) {
|
||||
// MarshalResourceChanges converts the provided internal representation of
|
||||
// ResourceInstanceChangeSrc objects into the public structured JSON changes.
|
||||
//
|
||||
// This function is referenced directly from the structured renderer tests, to
|
||||
// ensure parity between the renderers. It probably shouldn't be used anywhere
|
||||
// else.
|
||||
func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schemas *terraform.Schemas) ([]ResourceChange, error) {
|
||||
var ret []ResourceChange
|
||||
|
||||
for _, rc := range resources {
|
||||
@ -391,29 +412,29 @@ func (p *plan) marshalResourceChanges(resources []*plans.ResourceInstanceChangeS
|
||||
case plans.ResourceInstanceChangeNoReason:
|
||||
r.ActionReason = "" // will be omitted in output
|
||||
case plans.ResourceInstanceReplaceBecauseCannotUpdate:
|
||||
r.ActionReason = "replace_because_cannot_update"
|
||||
r.ActionReason = ResourceInstanceReplaceBecauseCannotUpdate
|
||||
case plans.ResourceInstanceReplaceBecauseTainted:
|
||||
r.ActionReason = "replace_because_tainted"
|
||||
r.ActionReason = ResourceInstanceReplaceBecauseTainted
|
||||
case plans.ResourceInstanceReplaceByRequest:
|
||||
r.ActionReason = "replace_by_request"
|
||||
r.ActionReason = ResourceInstanceReplaceByRequest
|
||||
case plans.ResourceInstanceReplaceByTriggers:
|
||||
r.ActionReason = "replace_by_triggers"
|
||||
r.ActionReason = ResourceInstanceReplaceByTriggers
|
||||
case plans.ResourceInstanceDeleteBecauseNoResourceConfig:
|
||||
r.ActionReason = "delete_because_no_resource_config"
|
||||
r.ActionReason = ResourceInstanceDeleteBecauseNoResourceConfig
|
||||
case plans.ResourceInstanceDeleteBecauseWrongRepetition:
|
||||
r.ActionReason = "delete_because_wrong_repetition"
|
||||
r.ActionReason = ResourceInstanceDeleteBecauseWrongRepetition
|
||||
case plans.ResourceInstanceDeleteBecauseCountIndex:
|
||||
r.ActionReason = "delete_because_count_index"
|
||||
r.ActionReason = ResourceInstanceDeleteBecauseCountIndex
|
||||
case plans.ResourceInstanceDeleteBecauseEachKey:
|
||||
r.ActionReason = "delete_because_each_key"
|
||||
r.ActionReason = ResourceInstanceDeleteBecauseEachKey
|
||||
case plans.ResourceInstanceDeleteBecauseNoModule:
|
||||
r.ActionReason = "delete_because_no_module"
|
||||
r.ActionReason = ResourceInstanceDeleteBecauseNoModule
|
||||
case plans.ResourceInstanceDeleteBecauseNoMoveTarget:
|
||||
r.ActionReason = "delete_because_no_move_target"
|
||||
r.ActionReason = ResourceInstanceDeleteBecauseNoMoveTarget
|
||||
case plans.ResourceInstanceReadBecauseConfigUnknown:
|
||||
r.ActionReason = "read_because_config_unknown"
|
||||
r.ActionReason = ResourceInstanceReadBecauseConfigUnknown
|
||||
case plans.ResourceInstanceReadBecauseDependencyPending:
|
||||
r.ActionReason = "read_because_dependency_pending"
|
||||
r.ActionReason = ResourceInstanceReadBecauseDependencyPending
|
||||
default:
|
||||
return nil, fmt.Errorf("resource %s has an unsupported action reason %s", r.Address, rc.ActionReason)
|
||||
}
|
||||
@ -429,13 +450,19 @@ func (p *plan) marshalResourceChanges(resources []*plans.ResourceInstanceChangeS
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (p *plan) marshalOutputChanges(changes *plans.Changes) error {
|
||||
// MarshalOutputChanges converts the provided internal representation of
|
||||
// Changes objects into the structured JSON representation.
|
||||
//
|
||||
// This function is referenced directly from the structured renderer tests, to
|
||||
// ensure parity between the renderers. It probably shouldn't be used anywhere
|
||||
// else.
|
||||
func MarshalOutputChanges(changes *plans.Changes) (map[string]Change, error) {
|
||||
if changes == nil {
|
||||
// Nothing to do!
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
p.OutputChanges = make(map[string]Change, len(changes.Outputs))
|
||||
outputChanges := make(map[string]Change, len(changes.Outputs))
|
||||
for _, oc := range changes.Outputs {
|
||||
|
||||
// Skip output changes that are not from the root module.
|
||||
@ -448,7 +475,7 @@ func (p *plan) marshalOutputChanges(changes *plans.Changes) error {
|
||||
|
||||
changeV, err := oc.Decode()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
// We drop the marks from the change, as decoding is only an
|
||||
// intermediate step to re-encode the values as json
|
||||
@ -461,14 +488,14 @@ func (p *plan) marshalOutputChanges(changes *plans.Changes) error {
|
||||
if changeV.Before != cty.NilVal {
|
||||
before, err = ctyjson.Marshal(changeV.Before, changeV.Before.Type())
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if changeV.After != cty.NilVal {
|
||||
if changeV.After.IsWhollyKnown() {
|
||||
after, err = ctyjson.Marshal(changeV.After, changeV.After.Type())
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
afterUnknown = cty.False
|
||||
} else {
|
||||
@ -478,7 +505,7 @@ func (p *plan) marshalOutputChanges(changes *plans.Changes) error {
|
||||
} else {
|
||||
after, err = ctyjson.Marshal(filteredAfter, filteredAfter.Type())
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
afterUnknown = unknownAsBool(changeV.After)
|
||||
@ -495,7 +522,7 @@ func (p *plan) marshalOutputChanges(changes *plans.Changes) error {
|
||||
}
|
||||
sensitive, err := ctyjson.Marshal(outputSensitive, outputSensitive.Type())
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a, _ := ctyjson.Marshal(afterUnknown, afterUnknown.Type())
|
||||
@ -509,10 +536,10 @@ func (p *plan) marshalOutputChanges(changes *plans.Changes) error {
|
||||
AfterSensitive: json.RawMessage(sensitive),
|
||||
}
|
||||
|
||||
p.OutputChanges[oc.Addr.OutputValue.Name] = c
|
||||
outputChanges[oc.Addr.OutputValue.Name] = c
|
||||
}
|
||||
|
||||
return nil
|
||||
return outputChanges, nil
|
||||
}
|
||||
|
||||
func (p *plan) marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) error {
|
||||
@ -698,6 +725,36 @@ func actionString(action string) []string {
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalActions reverses the actionString function.
|
||||
func UnmarshalActions(actions []string) plans.Action {
|
||||
if len(actions) == 2 {
|
||||
if actions[0] == "create" && actions[1] == "delete" {
|
||||
return plans.CreateThenDelete
|
||||
}
|
||||
|
||||
if actions[0] == "delete" && actions[1] == "create" {
|
||||
return plans.DeleteThenCreate
|
||||
}
|
||||
}
|
||||
|
||||
if len(actions) == 1 {
|
||||
switch actions[0] {
|
||||
case "create":
|
||||
return plans.Create
|
||||
case "delete":
|
||||
return plans.Delete
|
||||
case "update":
|
||||
return plans.Update
|
||||
case "read":
|
||||
return plans.Read
|
||||
case "no-op":
|
||||
return plans.NoOp
|
||||
}
|
||||
}
|
||||
|
||||
panic("unrecognized action slice: " + strings.Join(actions, ", "))
|
||||
}
|
||||
|
||||
// encodePaths lossily encodes a cty.PathSet into an array of arrays of step
|
||||
// values, such as:
|
||||
//
|
||||
|
@ -400,15 +400,13 @@ func TestOutputs(t *testing.T) {
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
p := newPlan()
|
||||
|
||||
err := p.marshalOutputChanges(test.changes)
|
||||
changes, err := MarshalOutputChanges(test.changes)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %s", err)
|
||||
}
|
||||
|
||||
if !cmp.Equal(p.OutputChanges, test.expected) {
|
||||
t.Errorf("wrong result:\n %v\n", cmp.Diff(p.OutputChanges, test.expected))
|
||||
if !cmp.Equal(changes, test.expected) {
|
||||
t.Errorf("wrong result:\n %v\n", cmp.Diff(changes, test.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -31,13 +31,21 @@ func newProviders() *providers {
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalForRenderer converts the provided internation representation of the
|
||||
// schema into the public structured JSON versions.
|
||||
//
|
||||
// This is a format that can be read by the structured plan renderer.
|
||||
func MarshalForRenderer(s *terraform.Schemas) map[string]*Provider {
|
||||
schemas := make(map[string]*Provider, len(s.Providers))
|
||||
for k, v := range s.Providers {
|
||||
schemas[k.String()] = marshalProvider(v)
|
||||
}
|
||||
return schemas
|
||||
}
|
||||
|
||||
func Marshal(s *terraform.Schemas) ([]byte, error) {
|
||||
providers := newProviders()
|
||||
|
||||
for k, v := range s.Providers {
|
||||
providers.Schemas[k.String()] = marshalProvider(v)
|
||||
}
|
||||
|
||||
providers.Schemas = MarshalForRenderer(s)
|
||||
ret, err := json.Marshal(providers)
|
||||
return ret, err
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user