1966 lines
60 KiB
Go
1966 lines
60 KiB
Go
package format
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/mitchellh/colorstring"
|
|
"github.com/zclconf/go-cty/cty"
|
|
ctyjson "github.com/zclconf/go-cty/cty/json"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform/internal/lang/marks"
|
|
"github.com/hashicorp/terraform/internal/plans"
|
|
"github.com/hashicorp/terraform/internal/plans/objchange"
|
|
"github.com/hashicorp/terraform/internal/states"
|
|
)
|
|
|
|
// DiffLanguage controls the description of the resource change reasons.
|
|
type DiffLanguage rune
|
|
|
|
//go:generate go run golang.org/x/tools/cmd/stringer -type=DiffLanguage diff.go
|
|
|
|
const (
|
|
// DiffLanguageProposedChange indicates that the change is one which is
|
|
// planned to be applied.
|
|
DiffLanguageProposedChange DiffLanguage = 'P'
|
|
|
|
// DiffLanguageDetectedDrift indicates that the change is detected drift
|
|
// from the configuration.
|
|
DiffLanguageDetectedDrift DiffLanguage = 'D'
|
|
)
|
|
|
|
// ResourceChange returns a string representation of a change to a particular
|
|
// resource, for inclusion in user-facing plan output.
|
|
//
|
|
// The resource schema must be provided along with the change so that the
|
|
// formatted change can reflect the configuration structure for the associated
|
|
// resource.
|
|
//
|
|
// If "color" is non-nil, it will be used to color the result. Otherwise,
|
|
// no color codes will be included.
|
|
func ResourceChange(
|
|
change *plans.ResourceInstanceChangeSrc,
|
|
schema *configschema.Block,
|
|
color *colorstring.Colorize,
|
|
language DiffLanguage,
|
|
) string {
|
|
addr := change.Addr
|
|
var buf bytes.Buffer
|
|
|
|
if color == nil {
|
|
color = &colorstring.Colorize{
|
|
Colors: colorstring.DefaultColors,
|
|
Disable: true,
|
|
Reset: false,
|
|
}
|
|
}
|
|
|
|
dispAddr := addr.String()
|
|
if change.DeposedKey != states.NotDeposed {
|
|
dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, change.DeposedKey)
|
|
}
|
|
|
|
switch change.Action {
|
|
case plans.Create:
|
|
buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be created"), dispAddr))
|
|
case plans.Read:
|
|
buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be read during apply\n # (config refers to values not yet known)"), dispAddr))
|
|
case plans.Update:
|
|
switch language {
|
|
case DiffLanguageProposedChange:
|
|
buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be updated in-place"), dispAddr))
|
|
case DiffLanguageDetectedDrift:
|
|
buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] has changed"), dispAddr))
|
|
default:
|
|
buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] update (unknown reason %s)"), dispAddr, language))
|
|
}
|
|
case plans.CreateThenDelete, plans.DeleteThenCreate:
|
|
switch change.ActionReason {
|
|
case plans.ResourceInstanceReplaceBecauseTainted:
|
|
buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] is tainted, so must be [bold][red]replaced"), dispAddr))
|
|
case plans.ResourceInstanceReplaceByRequest:
|
|
buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be [bold][red]replaced[reset], as requested"), dispAddr))
|
|
default:
|
|
buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] must be [bold][red]replaced"), dispAddr))
|
|
}
|
|
case plans.Delete:
|
|
switch language {
|
|
case DiffLanguageProposedChange:
|
|
buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be [bold][red]destroyed"), dispAddr))
|
|
case DiffLanguageDetectedDrift:
|
|
buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] has been deleted"), dispAddr))
|
|
default:
|
|
buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] delete (unknown reason %s)"), dispAddr, language))
|
|
}
|
|
if change.DeposedKey != states.NotDeposed {
|
|
// Some extra context about this unusual situation.
|
|
buf.WriteString(color.Color("\n # (left over from a partially-failed replacement of this instance)"))
|
|
}
|
|
case plans.NoOp:
|
|
if change.Moved() {
|
|
buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] has moved to [bold]%s[reset]"), change.PrevRunAddr.String(), 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(color.Color("[reset]\n"))
|
|
|
|
if change.Moved() && change.Action != plans.NoOp {
|
|
buf.WriteString(fmt.Sprintf(color.Color("[bold] # [reset]([bold]%s[reset] has moved to [bold]%s[reset])\n"), change.PrevRunAddr.String(), dispAddr))
|
|
}
|
|
|
|
if change.Moved() && change.Action == plans.NoOp {
|
|
buf.WriteString(" ")
|
|
} else {
|
|
buf.WriteString(color.Color(DiffActionSymbol(change.Action)) + " ")
|
|
}
|
|
|
|
switch addr.Resource.Resource.Mode {
|
|
case addrs.ManagedResourceMode:
|
|
buf.WriteString(fmt.Sprintf(
|
|
"resource %q %q",
|
|
addr.Resource.Resource.Type,
|
|
addr.Resource.Resource.Name,
|
|
))
|
|
case addrs.DataResourceMode:
|
|
buf.WriteString(fmt.Sprintf(
|
|
"data %q %q ",
|
|
addr.Resource.Resource.Type,
|
|
addr.Resource.Resource.Name,
|
|
))
|
|
default:
|
|
// should never happen, since the above is exhaustive
|
|
buf.WriteString(addr.String())
|
|
}
|
|
|
|
buf.WriteString(" {")
|
|
|
|
p := blockBodyDiffPrinter{
|
|
buf: &buf,
|
|
color: color,
|
|
action: change.Action,
|
|
requiredReplace: change.RequiredReplace,
|
|
}
|
|
|
|
// Most commonly-used resources have nested blocks that result in us
|
|
// going at least three traversals deep while we recurse here, so we'll
|
|
// start with that much capacity and then grow as needed for deeper
|
|
// structures.
|
|
path := make(cty.Path, 0, 3)
|
|
|
|
changeV, err := change.Decode(schema.ImpliedType())
|
|
if err != nil {
|
|
// Should never happen in here, since we've already been through
|
|
// loads of layers of encode/decode of the planned changes before now.
|
|
panic(fmt.Sprintf("failed to decode plan for %s while rendering diff: %s", addr, err))
|
|
}
|
|
|
|
// We currently have an opt-out that permits the legacy SDK to return values
|
|
// that defy our usual conventions around handling of nesting blocks. To
|
|
// avoid the rendering code from needing to handle all of these, we'll
|
|
// normalize first.
|
|
// (Ideally we'd do this as part of the SDK opt-out implementation in core,
|
|
// but we've added it here for now to reduce risk of unexpected impacts
|
|
// on other code in core.)
|
|
changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, schema)
|
|
changeV.Change.After = objchange.NormalizeObjectFromLegacySDK(changeV.Change.After, schema)
|
|
|
|
result := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path)
|
|
if result.bodyWritten {
|
|
buf.WriteString("\n")
|
|
buf.WriteString(strings.Repeat(" ", 4))
|
|
}
|
|
buf.WriteString("}\n")
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
// OutputChanges returns a string representation of a set of changes to output
|
|
// values for inclusion in user-facing plan output.
|
|
//
|
|
// If "color" is non-nil, it will be used to color the result. Otherwise,
|
|
// no color codes will be included.
|
|
func OutputChanges(
|
|
changes []*plans.OutputChangeSrc,
|
|
color *colorstring.Colorize,
|
|
) string {
|
|
var buf bytes.Buffer
|
|
p := blockBodyDiffPrinter{
|
|
buf: &buf,
|
|
color: color,
|
|
action: plans.Update, // not actually used in this case, because we're not printing a containing block
|
|
}
|
|
|
|
// We're going to reuse the codepath we used for printing resource block
|
|
// diffs, by pretending that the set of defined outputs are the attributes
|
|
// of some resource. It's a little forced to do this, but it gives us all
|
|
// the same formatting heuristics as we normally use for resource
|
|
// attributes.
|
|
oldVals := make(map[string]cty.Value, len(changes))
|
|
newVals := make(map[string]cty.Value, len(changes))
|
|
synthSchema := &configschema.Block{
|
|
Attributes: make(map[string]*configschema.Attribute, len(changes)),
|
|
}
|
|
for _, changeSrc := range changes {
|
|
name := changeSrc.Addr.OutputValue.Name
|
|
change, err := changeSrc.Decode()
|
|
if err != nil {
|
|
// It'd be weird to get a decoding error here because that would
|
|
// suggest that Terraform itself just produced an invalid plan, and
|
|
// we don't have any good way to ignore it in this codepath, so
|
|
// we'll just log it and ignore it.
|
|
log.Printf("[ERROR] format.OutputChanges: Failed to decode planned change for output %q: %s", name, err)
|
|
continue
|
|
}
|
|
synthSchema.Attributes[name] = &configschema.Attribute{
|
|
Type: cty.DynamicPseudoType, // output types are decided dynamically based on the given value
|
|
Optional: true,
|
|
Sensitive: change.Sensitive,
|
|
}
|
|
oldVals[name] = change.Before
|
|
newVals[name] = change.After
|
|
}
|
|
|
|
p.writeBlockBodyDiff(synthSchema, cty.ObjectVal(oldVals), cty.ObjectVal(newVals), 2, nil)
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
type blockBodyDiffPrinter struct {
|
|
buf *bytes.Buffer
|
|
color *colorstring.Colorize
|
|
action plans.Action
|
|
requiredReplace cty.PathSet
|
|
// verbose is set to true when using the "diff" printer to format state
|
|
verbose bool
|
|
}
|
|
|
|
type blockBodyDiffResult struct {
|
|
bodyWritten bool
|
|
skippedAttributes int
|
|
skippedBlocks int
|
|
}
|
|
|
|
const forcesNewResourceCaption = " [red]# forces replacement[reset]"
|
|
|
|
// writeBlockBodyDiff writes attribute or block differences
|
|
// and returns true if any differences were found and written
|
|
func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, old, new cty.Value, indent int, path cty.Path) blockBodyDiffResult {
|
|
path = ctyEnsurePathCapacity(path, 1)
|
|
result := blockBodyDiffResult{}
|
|
|
|
// write the attributes diff
|
|
blankBeforeBlocks := p.writeAttrsDiff(schema.Attributes, old, new, indent, path, &result)
|
|
p.writeSkippedAttr(result.skippedAttributes, indent+2)
|
|
|
|
{
|
|
blockTypeNames := make([]string, 0, len(schema.BlockTypes))
|
|
for name := range schema.BlockTypes {
|
|
blockTypeNames = append(blockTypeNames, name)
|
|
}
|
|
sort.Strings(blockTypeNames)
|
|
|
|
for _, name := range blockTypeNames {
|
|
blockS := schema.BlockTypes[name]
|
|
oldVal := ctyGetAttrMaybeNull(old, name)
|
|
newVal := ctyGetAttrMaybeNull(new, name)
|
|
|
|
result.bodyWritten = true
|
|
skippedBlocks := p.writeNestedBlockDiffs(name, blockS, oldVal, newVal, blankBeforeBlocks, indent, path)
|
|
if skippedBlocks > 0 {
|
|
result.skippedBlocks += skippedBlocks
|
|
}
|
|
|
|
// Always include a blank for any subsequent block types.
|
|
blankBeforeBlocks = true
|
|
}
|
|
if result.skippedBlocks > 0 {
|
|
noun := "blocks"
|
|
if result.skippedBlocks == 1 {
|
|
noun = "block"
|
|
}
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), result.skippedBlocks, noun))
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (p *blockBodyDiffPrinter) writeAttrsDiff(
|
|
attrsS map[string]*configschema.Attribute,
|
|
old, new cty.Value,
|
|
indent int,
|
|
path cty.Path,
|
|
result *blockBodyDiffResult) bool {
|
|
|
|
blankBeforeBlocks := false
|
|
|
|
attrNames := make([]string, 0, len(attrsS))
|
|
attrNameLen := 0
|
|
for name := range attrsS {
|
|
oldVal := ctyGetAttrMaybeNull(old, name)
|
|
newVal := ctyGetAttrMaybeNull(new, name)
|
|
if oldVal.IsNull() && newVal.IsNull() {
|
|
// Skip attributes where both old and new values are null
|
|
// (we do this early here so that we'll do our value alignment
|
|
// based on the longest attribute name that has a change, rather
|
|
// than the longest attribute name in the full set.)
|
|
continue
|
|
}
|
|
|
|
attrNames = append(attrNames, name)
|
|
if len(name) > attrNameLen {
|
|
attrNameLen = len(name)
|
|
}
|
|
}
|
|
sort.Strings(attrNames)
|
|
if len(attrNames) > 0 {
|
|
blankBeforeBlocks = true
|
|
}
|
|
|
|
for _, name := range attrNames {
|
|
attrS := attrsS[name]
|
|
oldVal := ctyGetAttrMaybeNull(old, name)
|
|
newVal := ctyGetAttrMaybeNull(new, name)
|
|
|
|
result.bodyWritten = true
|
|
skipped := p.writeAttrDiff(name, attrS, oldVal, newVal, attrNameLen, indent, path)
|
|
if skipped {
|
|
result.skippedAttributes++
|
|
}
|
|
}
|
|
|
|
return blankBeforeBlocks
|
|
}
|
|
|
|
// getPlanActionAndShow returns the action value
|
|
// and a boolean for showJustNew. In this function we
|
|
// modify the old and new values to remove any possible marks
|
|
func getPlanActionAndShow(old cty.Value, new cty.Value) (plans.Action, bool) {
|
|
var action plans.Action
|
|
showJustNew := false
|
|
switch {
|
|
case old.IsNull():
|
|
action = plans.Create
|
|
showJustNew = true
|
|
case new.IsNull():
|
|
action = plans.Delete
|
|
case ctyEqualWithUnknown(old, new):
|
|
action = plans.NoOp
|
|
showJustNew = true
|
|
default:
|
|
action = plans.Update
|
|
}
|
|
return action, showJustNew
|
|
}
|
|
|
|
func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) bool {
|
|
path = append(path, cty.GetAttrStep{Name: name})
|
|
action, showJustNew := getPlanActionAndShow(old, new)
|
|
|
|
if action == plans.NoOp && !p.verbose && !identifyingAttribute(name, attrS) {
|
|
return true
|
|
}
|
|
|
|
if attrS.NestedType != nil {
|
|
p.writeNestedAttrDiff(name, attrS.NestedType, old, new, nameLen, indent, path, action, showJustNew)
|
|
return false
|
|
}
|
|
|
|
p.buf.WriteString("\n")
|
|
|
|
p.writeSensitivityWarning(old, new, indent, action, false)
|
|
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.writeActionSymbol(action)
|
|
|
|
p.buf.WriteString(p.color.Color("[bold]"))
|
|
p.buf.WriteString(name)
|
|
p.buf.WriteString(p.color.Color("[reset]"))
|
|
p.buf.WriteString(strings.Repeat(" ", nameLen-len(name)))
|
|
p.buf.WriteString(" = ")
|
|
|
|
if attrS.Sensitive {
|
|
p.buf.WriteString("(sensitive value)")
|
|
if p.pathForcesNewResource(path) {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
} else {
|
|
switch {
|
|
case showJustNew:
|
|
p.writeValue(new, action, indent+2)
|
|
if p.pathForcesNewResource(path) {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
default:
|
|
// We show new even if it is null to emphasize the fact
|
|
// that it is being unset, since otherwise it is easy to
|
|
// misunderstand that the value is still set to the old value.
|
|
p.writeValueDiff(old, new, indent+2, path)
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// writeNestedAttrDiff is responsible for formatting Attributes with NestedTypes
|
|
// in the diff.
|
|
func (p *blockBodyDiffPrinter) writeNestedAttrDiff(
|
|
name string, objS *configschema.Object, old, new cty.Value,
|
|
nameLen, indent int, path cty.Path, action plans.Action, showJustNew bool) {
|
|
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.writeActionSymbol(action)
|
|
|
|
p.buf.WriteString(p.color.Color("[bold]"))
|
|
p.buf.WriteString(name)
|
|
p.buf.WriteString(p.color.Color("[reset]"))
|
|
p.buf.WriteString(strings.Repeat(" ", nameLen-len(name)))
|
|
|
|
result := &blockBodyDiffResult{}
|
|
switch objS.Nesting {
|
|
case configschema.NestingSingle:
|
|
p.buf.WriteString(" = {")
|
|
if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
p.writeAttrsDiff(objS.Attributes, old, new, indent+2, path, result)
|
|
p.writeSkippedAttr(result.skippedAttributes, indent+4)
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.buf.WriteString("}")
|
|
|
|
case configschema.NestingList:
|
|
p.buf.WriteString(" = [")
|
|
if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent+4))
|
|
p.writeActionSymbol(action)
|
|
p.buf.WriteString("{")
|
|
|
|
oldItems := ctyCollectionValues(old)
|
|
newItems := ctyCollectionValues(new)
|
|
// Here we intentionally preserve the index-based correspondance
|
|
// between old and new, rather than trying to detect insertions
|
|
// and removals in the list, because this more accurately reflects
|
|
// how Terraform Core and providers will understand the change,
|
|
// particularly when the nested block contains computed attributes
|
|
// that will themselves maintain correspondance by index.
|
|
|
|
// commonLen is number of elements that exist in both lists, which
|
|
// will be presented as updates (~). Any additional items in one
|
|
// of the lists will be presented as either creates (+) or deletes (-)
|
|
// depending on which list they belong to.
|
|
var commonLen int
|
|
// unchanged is the number of unchanged elements
|
|
var unchanged int
|
|
|
|
switch {
|
|
case len(oldItems) < len(newItems):
|
|
commonLen = len(oldItems)
|
|
default:
|
|
commonLen = len(newItems)
|
|
}
|
|
for i := 0; i < commonLen; i++ {
|
|
path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
|
|
oldItem := oldItems[i]
|
|
newItem := newItems[i]
|
|
if oldItem.RawEquals(newItem) {
|
|
action = plans.NoOp
|
|
unchanged++
|
|
}
|
|
if action != plans.NoOp {
|
|
p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+6, path, result)
|
|
p.writeSkippedAttr(result.skippedAttributes, indent+8)
|
|
p.buf.WriteString("\n")
|
|
}
|
|
}
|
|
for i := commonLen; i < len(oldItems); i++ {
|
|
path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
|
|
oldItem := oldItems[i]
|
|
newItem := cty.NullVal(oldItem.Type())
|
|
p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+6, path, result)
|
|
p.buf.WriteString("\n")
|
|
}
|
|
for i := commonLen; i < len(newItems); i++ {
|
|
path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
|
|
newItem := newItems[i]
|
|
oldItem := cty.NullVal(newItem.Type())
|
|
p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+6, path, result)
|
|
p.buf.WriteString("\n")
|
|
}
|
|
p.buf.WriteString(strings.Repeat(" ", indent+4))
|
|
p.buf.WriteString("},\n")
|
|
p.writeSkippedElems(unchanged, indent+4)
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
p.buf.WriteString("]")
|
|
|
|
if !new.IsKnown() {
|
|
p.buf.WriteString(" -> (known after apply)")
|
|
}
|
|
|
|
case configschema.NestingSet:
|
|
oldItems := ctyCollectionValues(old)
|
|
newItems := ctyCollectionValues(new)
|
|
|
|
var all cty.Value
|
|
if len(oldItems)+len(newItems) > 0 {
|
|
allItems := make([]cty.Value, 0, len(oldItems)+len(newItems))
|
|
allItems = append(allItems, oldItems...)
|
|
allItems = append(allItems, newItems...)
|
|
|
|
all = cty.SetVal(allItems)
|
|
} else {
|
|
all = cty.SetValEmpty(old.Type().ElementType())
|
|
}
|
|
|
|
p.buf.WriteString(" = [")
|
|
|
|
for it := all.ElementIterator(); it.Next(); {
|
|
_, val := it.Element()
|
|
var action plans.Action
|
|
var oldValue, newValue cty.Value
|
|
switch {
|
|
case !val.IsKnown():
|
|
action = plans.Update
|
|
newValue = val
|
|
case !new.IsKnown():
|
|
action = plans.Delete
|
|
// the value must have come from the old set
|
|
oldValue = val
|
|
// Mark the new val as null, but the entire set will be
|
|
// displayed as "(unknown after apply)"
|
|
newValue = cty.NullVal(val.Type())
|
|
case old.IsNull() || !old.HasElement(val).True():
|
|
action = plans.Create
|
|
oldValue = cty.NullVal(val.Type())
|
|
newValue = val
|
|
case new.IsNull() || !new.HasElement(val).True():
|
|
action = plans.Delete
|
|
oldValue = val
|
|
newValue = cty.NullVal(val.Type())
|
|
default:
|
|
action = plans.NoOp
|
|
oldValue = val
|
|
newValue = val
|
|
}
|
|
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent+4))
|
|
p.writeActionSymbol(action)
|
|
p.buf.WriteString("{")
|
|
|
|
if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
|
|
path := append(path, cty.IndexStep{Key: val})
|
|
p.writeAttrsDiff(objS.Attributes, oldValue, newValue, indent+6, path, result)
|
|
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent+4))
|
|
p.buf.WriteString("},")
|
|
}
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
p.buf.WriteString("]")
|
|
|
|
if !new.IsKnown() {
|
|
p.buf.WriteString(" -> (known after apply)")
|
|
}
|
|
|
|
case configschema.NestingMap:
|
|
// For the sake of handling nested blocks, we'll treat a null map
|
|
// the same as an empty map since the config language doesn't
|
|
// distinguish these anyway.
|
|
old = ctyNullBlockMapAsEmpty(old)
|
|
new = ctyNullBlockMapAsEmpty(new)
|
|
|
|
oldItems := old.AsValueMap()
|
|
|
|
newItems := map[string]cty.Value{}
|
|
|
|
if new.IsKnown() {
|
|
newItems = new.AsValueMap()
|
|
}
|
|
|
|
allKeys := make(map[string]bool)
|
|
for k := range oldItems {
|
|
allKeys[k] = true
|
|
}
|
|
for k := range newItems {
|
|
allKeys[k] = true
|
|
}
|
|
allKeysOrder := make([]string, 0, len(allKeys))
|
|
for k := range allKeys {
|
|
allKeysOrder = append(allKeysOrder, k)
|
|
}
|
|
sort.Strings(allKeysOrder)
|
|
|
|
p.buf.WriteString(" = {\n")
|
|
|
|
// unchanged tracks the number of unchanged elements
|
|
unchanged := 0
|
|
for _, k := range allKeysOrder {
|
|
var action plans.Action
|
|
oldValue := oldItems[k]
|
|
|
|
newValue := newItems[k]
|
|
switch {
|
|
case oldValue == cty.NilVal:
|
|
oldValue = cty.NullVal(newValue.Type())
|
|
action = plans.Create
|
|
case newValue == cty.NilVal:
|
|
newValue = cty.NullVal(oldValue.Type())
|
|
action = plans.Delete
|
|
case !newValue.RawEquals(oldValue):
|
|
action = plans.Update
|
|
default:
|
|
action = plans.NoOp
|
|
unchanged++
|
|
}
|
|
|
|
if action != plans.NoOp {
|
|
p.buf.WriteString(strings.Repeat(" ", indent+4))
|
|
p.writeActionSymbol(action)
|
|
fmt.Fprintf(p.buf, "%q = {", k)
|
|
if p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1]) {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
|
|
path := append(path, cty.IndexStep{Key: cty.StringVal(k)})
|
|
p.writeAttrsDiff(objS.Attributes, oldValue, newValue, indent+6, path, result)
|
|
p.writeSkippedAttr(result.skippedAttributes, indent+8)
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent+4))
|
|
p.buf.WriteString("},\n")
|
|
}
|
|
}
|
|
|
|
p.writeSkippedElems(unchanged, indent+4)
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
p.buf.WriteString("}")
|
|
if !new.IsKnown() {
|
|
p.buf.WriteString(" -> (known after apply)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *configschema.NestedBlock, old, new cty.Value, blankBefore bool, indent int, path cty.Path) int {
|
|
skippedBlocks := 0
|
|
path = append(path, cty.GetAttrStep{Name: name})
|
|
if old.IsNull() && new.IsNull() {
|
|
// Nothing to do if both old and new is null
|
|
return skippedBlocks
|
|
}
|
|
|
|
// If either the old or the new value is marked,
|
|
// Display a special diff because it is irrelevant
|
|
// to list all obfuscated attributes as (sensitive)
|
|
if old.HasMark(marks.Sensitive) || new.HasMark(marks.Sensitive) {
|
|
p.writeSensitiveNestedBlockDiff(name, old, new, indent, blankBefore, path)
|
|
return 0
|
|
}
|
|
|
|
// Where old/new are collections representing a nesting mode other than
|
|
// NestingSingle, we assume the collection value can never be unknown
|
|
// since we always produce the container for the nested objects, even if
|
|
// the objects within are computed.
|
|
|
|
switch blockS.Nesting {
|
|
case configschema.NestingSingle, configschema.NestingGroup:
|
|
var action plans.Action
|
|
eqV := new.Equals(old)
|
|
switch {
|
|
case old.IsNull():
|
|
action = plans.Create
|
|
case new.IsNull():
|
|
action = plans.Delete
|
|
case !new.IsWhollyKnown() || !old.IsWhollyKnown():
|
|
// "old" should actually always be known due to our contract
|
|
// that old values must never be unknown, but we'll allow it
|
|
// anyway to be robust.
|
|
action = plans.Update
|
|
case !eqV.IsKnown() || !eqV.True():
|
|
action = plans.Update
|
|
}
|
|
|
|
if blankBefore {
|
|
p.buf.WriteRune('\n')
|
|
}
|
|
skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, old, new, indent, path)
|
|
if skipped {
|
|
return 1
|
|
}
|
|
case configschema.NestingList:
|
|
// For the sake of handling nested blocks, we'll treat a null list
|
|
// the same as an empty list since the config language doesn't
|
|
// distinguish these anyway.
|
|
old = ctyNullBlockListAsEmpty(old)
|
|
new = ctyNullBlockListAsEmpty(new)
|
|
|
|
oldItems := ctyCollectionValues(old)
|
|
newItems := ctyCollectionValues(new)
|
|
|
|
// Here we intentionally preserve the index-based correspondance
|
|
// between old and new, rather than trying to detect insertions
|
|
// and removals in the list, because this more accurately reflects
|
|
// how Terraform Core and providers will understand the change,
|
|
// particularly when the nested block contains computed attributes
|
|
// that will themselves maintain correspondance by index.
|
|
|
|
// commonLen is number of elements that exist in both lists, which
|
|
// will be presented as updates (~). Any additional items in one
|
|
// of the lists will be presented as either creates (+) or deletes (-)
|
|
// depending on which list they belong to.
|
|
var commonLen int
|
|
switch {
|
|
case len(oldItems) < len(newItems):
|
|
commonLen = len(oldItems)
|
|
default:
|
|
commonLen = len(newItems)
|
|
}
|
|
|
|
if blankBefore && (len(oldItems) > 0 || len(newItems) > 0) {
|
|
p.buf.WriteRune('\n')
|
|
}
|
|
|
|
for i := 0; i < commonLen; i++ {
|
|
path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
|
|
oldItem := oldItems[i]
|
|
newItem := newItems[i]
|
|
action := plans.Update
|
|
if oldItem.RawEquals(newItem) {
|
|
action = plans.NoOp
|
|
}
|
|
skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldItem, newItem, indent, path)
|
|
if skipped {
|
|
skippedBlocks++
|
|
}
|
|
}
|
|
for i := commonLen; i < len(oldItems); i++ {
|
|
path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
|
|
oldItem := oldItems[i]
|
|
newItem := cty.NullVal(oldItem.Type())
|
|
skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Delete, oldItem, newItem, indent, path)
|
|
if skipped {
|
|
skippedBlocks++
|
|
}
|
|
}
|
|
for i := commonLen; i < len(newItems); i++ {
|
|
path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
|
|
newItem := newItems[i]
|
|
oldItem := cty.NullVal(newItem.Type())
|
|
skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Create, oldItem, newItem, indent, path)
|
|
if skipped {
|
|
skippedBlocks++
|
|
}
|
|
}
|
|
case configschema.NestingSet:
|
|
// For the sake of handling nested blocks, we'll treat a null set
|
|
// the same as an empty set since the config language doesn't
|
|
// distinguish these anyway.
|
|
old = ctyNullBlockSetAsEmpty(old)
|
|
new = ctyNullBlockSetAsEmpty(new)
|
|
|
|
oldItems := ctyCollectionValues(old)
|
|
newItems := ctyCollectionValues(new)
|
|
|
|
if (len(oldItems) + len(newItems)) == 0 {
|
|
// Nothing to do if both sets are empty
|
|
return 0
|
|
}
|
|
|
|
allItems := make([]cty.Value, 0, len(oldItems)+len(newItems))
|
|
allItems = append(allItems, oldItems...)
|
|
allItems = append(allItems, newItems...)
|
|
all := cty.SetVal(allItems)
|
|
|
|
if blankBefore {
|
|
p.buf.WriteRune('\n')
|
|
}
|
|
|
|
for it := all.ElementIterator(); it.Next(); {
|
|
_, val := it.Element()
|
|
var action plans.Action
|
|
var oldValue, newValue cty.Value
|
|
switch {
|
|
case !val.IsKnown():
|
|
action = plans.Update
|
|
newValue = val
|
|
case !old.HasElement(val).True():
|
|
action = plans.Create
|
|
oldValue = cty.NullVal(val.Type())
|
|
newValue = val
|
|
case !new.HasElement(val).True():
|
|
action = plans.Delete
|
|
oldValue = val
|
|
newValue = cty.NullVal(val.Type())
|
|
default:
|
|
action = plans.NoOp
|
|
oldValue = val
|
|
newValue = val
|
|
}
|
|
path := append(path, cty.IndexStep{Key: val})
|
|
skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldValue, newValue, indent, path)
|
|
if skipped {
|
|
skippedBlocks++
|
|
}
|
|
}
|
|
|
|
case configschema.NestingMap:
|
|
// For the sake of handling nested blocks, we'll treat a null map
|
|
// the same as an empty map since the config language doesn't
|
|
// distinguish these anyway.
|
|
old = ctyNullBlockMapAsEmpty(old)
|
|
new = ctyNullBlockMapAsEmpty(new)
|
|
|
|
oldItems := old.AsValueMap()
|
|
newItems := new.AsValueMap()
|
|
if (len(oldItems) + len(newItems)) == 0 {
|
|
// Nothing to do if both maps are empty
|
|
return 0
|
|
}
|
|
|
|
allKeys := make(map[string]bool)
|
|
for k := range oldItems {
|
|
allKeys[k] = true
|
|
}
|
|
for k := range newItems {
|
|
allKeys[k] = true
|
|
}
|
|
allKeysOrder := make([]string, 0, len(allKeys))
|
|
for k := range allKeys {
|
|
allKeysOrder = append(allKeysOrder, k)
|
|
}
|
|
sort.Strings(allKeysOrder)
|
|
|
|
if blankBefore {
|
|
p.buf.WriteRune('\n')
|
|
}
|
|
|
|
for _, k := range allKeysOrder {
|
|
var action plans.Action
|
|
oldValue := oldItems[k]
|
|
newValue := newItems[k]
|
|
switch {
|
|
case oldValue == cty.NilVal:
|
|
oldValue = cty.NullVal(newValue.Type())
|
|
action = plans.Create
|
|
case newValue == cty.NilVal:
|
|
newValue = cty.NullVal(oldValue.Type())
|
|
action = plans.Delete
|
|
case !newValue.RawEquals(oldValue):
|
|
action = plans.Update
|
|
default:
|
|
action = plans.NoOp
|
|
}
|
|
|
|
path := append(path, cty.IndexStep{Key: cty.StringVal(k)})
|
|
skipped := p.writeNestedBlockDiff(name, &k, &blockS.Block, action, oldValue, newValue, indent, path)
|
|
if skipped {
|
|
skippedBlocks++
|
|
}
|
|
}
|
|
}
|
|
return skippedBlocks
|
|
}
|
|
|
|
func (p *blockBodyDiffPrinter) writeSensitiveNestedBlockDiff(name string, old, new cty.Value, indent int, blankBefore bool, path cty.Path) {
|
|
var action plans.Action
|
|
switch {
|
|
case old.IsNull():
|
|
action = plans.Create
|
|
case new.IsNull():
|
|
action = plans.Delete
|
|
case !new.IsWhollyKnown() || !old.IsWhollyKnown():
|
|
// "old" should actually always be known due to our contract
|
|
// that old values must never be unknown, but we'll allow it
|
|
// anyway to be robust.
|
|
action = plans.Update
|
|
case !ctyEqualValueAndMarks(old, new):
|
|
action = plans.Update
|
|
}
|
|
|
|
if blankBefore {
|
|
p.buf.WriteRune('\n')
|
|
}
|
|
|
|
// New line before warning printing
|
|
p.buf.WriteRune('\n')
|
|
p.writeSensitivityWarning(old, new, indent, action, true)
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.writeActionSymbol(action)
|
|
fmt.Fprintf(p.buf, "%s {", name)
|
|
if action != plans.NoOp && p.pathForcesNewResource(path) {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
p.buf.WriteRune('\n')
|
|
p.buf.WriteString(strings.Repeat(" ", indent+4))
|
|
p.buf.WriteString("# At least one attribute in this block is (or was) sensitive,\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent+4))
|
|
p.buf.WriteString("# so its contents will not be displayed.")
|
|
p.buf.WriteRune('\n')
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
p.buf.WriteString("}")
|
|
}
|
|
|
|
func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string, blockS *configschema.Block, action plans.Action, old, new cty.Value, indent int, path cty.Path) bool {
|
|
if action == plans.NoOp && !p.verbose {
|
|
return true
|
|
}
|
|
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.writeActionSymbol(action)
|
|
|
|
if label != nil {
|
|
fmt.Fprintf(p.buf, "%s %q {", name, *label)
|
|
} else {
|
|
fmt.Fprintf(p.buf, "%s {", name)
|
|
}
|
|
|
|
if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
|
|
result := p.writeBlockBodyDiff(blockS, old, new, indent+4, path)
|
|
if result.bodyWritten {
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
}
|
|
p.buf.WriteString("}")
|
|
|
|
return false
|
|
}
|
|
|
|
func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) {
|
|
// Could check specifically for the sensitivity marker
|
|
if val.HasMark(marks.Sensitive) {
|
|
p.buf.WriteString("(sensitive)")
|
|
return
|
|
}
|
|
|
|
if !val.IsKnown() {
|
|
p.buf.WriteString("(known after apply)")
|
|
return
|
|
}
|
|
if val.IsNull() {
|
|
p.buf.WriteString(p.color.Color("[dark_gray]null[reset]"))
|
|
return
|
|
}
|
|
|
|
ty := val.Type()
|
|
|
|
switch {
|
|
case ty.IsPrimitiveType():
|
|
switch ty {
|
|
case cty.String:
|
|
{
|
|
// Special behavior for JSON strings containing array or object
|
|
src := []byte(val.AsString())
|
|
ty, err := ctyjson.ImpliedType(src)
|
|
// check for the special case of "null", which decodes to nil,
|
|
// and just allow it to be printed out directly
|
|
if err == nil && !ty.IsPrimitiveType() && strings.TrimSpace(val.AsString()) != "null" {
|
|
jv, err := ctyjson.Unmarshal(src, ty)
|
|
if err == nil {
|
|
p.buf.WriteString("jsonencode(")
|
|
if jv.LengthInt() == 0 {
|
|
p.writeValue(jv, action, 0)
|
|
} else {
|
|
p.buf.WriteByte('\n')
|
|
p.buf.WriteString(strings.Repeat(" ", indent+4))
|
|
p.writeValue(jv, action, indent+4)
|
|
p.buf.WriteByte('\n')
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
}
|
|
p.buf.WriteByte(')')
|
|
break // don't *also* do the normal behavior below
|
|
}
|
|
}
|
|
}
|
|
|
|
if strings.Contains(val.AsString(), "\n") {
|
|
// It's a multi-line string, so we want to use the multi-line
|
|
// rendering so it'll be readable. Rather than re-implement
|
|
// that here, we'll just re-use the multi-line string diff
|
|
// printer with no changes, which ends up producing the
|
|
// result we want here.
|
|
// The path argument is nil because we don't track path
|
|
// information into strings and we know that a string can't
|
|
// have any indices or attributes that might need to be marked
|
|
// as (requires replacement), which is what that argument is for.
|
|
p.writeValueDiff(val, val, indent, nil)
|
|
break
|
|
}
|
|
|
|
fmt.Fprintf(p.buf, "%q", val.AsString())
|
|
case cty.Bool:
|
|
if val.True() {
|
|
p.buf.WriteString("true")
|
|
} else {
|
|
p.buf.WriteString("false")
|
|
}
|
|
case cty.Number:
|
|
bf := val.AsBigFloat()
|
|
p.buf.WriteString(bf.Text('f', -1))
|
|
default:
|
|
// should never happen, since the above is exhaustive
|
|
fmt.Fprintf(p.buf, "%#v", val)
|
|
}
|
|
case ty.IsListType() || ty.IsSetType() || ty.IsTupleType():
|
|
p.buf.WriteString("[")
|
|
|
|
it := val.ElementIterator()
|
|
for it.Next() {
|
|
_, val := it.Element()
|
|
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
p.writeActionSymbol(action)
|
|
p.writeValue(val, action, indent+4)
|
|
p.buf.WriteString(",")
|
|
}
|
|
|
|
if val.LengthInt() > 0 {
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
}
|
|
p.buf.WriteString("]")
|
|
case ty.IsMapType():
|
|
p.buf.WriteString("{")
|
|
|
|
keyLen := 0
|
|
for it := val.ElementIterator(); it.Next(); {
|
|
key, _ := it.Element()
|
|
if keyStr := key.AsString(); len(keyStr) > keyLen {
|
|
keyLen = len(keyStr)
|
|
}
|
|
}
|
|
|
|
for it := val.ElementIterator(); it.Next(); {
|
|
key, val := it.Element()
|
|
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
p.writeActionSymbol(action)
|
|
p.writeValue(key, action, indent+4)
|
|
p.buf.WriteString(strings.Repeat(" ", keyLen-len(key.AsString())))
|
|
p.buf.WriteString(" = ")
|
|
p.writeValue(val, action, indent+4)
|
|
}
|
|
|
|
if val.LengthInt() > 0 {
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
}
|
|
p.buf.WriteString("}")
|
|
case ty.IsObjectType():
|
|
p.buf.WriteString("{")
|
|
|
|
atys := ty.AttributeTypes()
|
|
attrNames := make([]string, 0, len(atys))
|
|
nameLen := 0
|
|
for attrName := range atys {
|
|
attrNames = append(attrNames, attrName)
|
|
if len(attrName) > nameLen {
|
|
nameLen = len(attrName)
|
|
}
|
|
}
|
|
sort.Strings(attrNames)
|
|
|
|
for _, attrName := range attrNames {
|
|
val := val.GetAttr(attrName)
|
|
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
p.writeActionSymbol(action)
|
|
p.buf.WriteString(attrName)
|
|
p.buf.WriteString(strings.Repeat(" ", nameLen-len(attrName)))
|
|
p.buf.WriteString(" = ")
|
|
p.writeValue(val, action, indent+4)
|
|
}
|
|
|
|
if len(attrNames) > 0 {
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
}
|
|
p.buf.WriteString("}")
|
|
}
|
|
}
|
|
|
|
func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, path cty.Path) {
|
|
ty := old.Type()
|
|
typesEqual := ctyTypesEqual(ty, new.Type())
|
|
|
|
// We have some specialized diff implementations for certain complex
|
|
// values where it's useful to see a visualization of the diff of
|
|
// the nested elements rather than just showing the entire old and
|
|
// new values verbatim.
|
|
// However, these specialized implementations can apply only if both
|
|
// values are known and non-null.
|
|
if old.IsKnown() && new.IsKnown() && !old.IsNull() && !new.IsNull() && typesEqual {
|
|
if old.HasMark(marks.Sensitive) || new.HasMark(marks.Sensitive) {
|
|
p.buf.WriteString("(sensitive)")
|
|
if p.pathForcesNewResource(path) {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case ty == cty.String:
|
|
// We have special behavior for both multi-line strings in general
|
|
// and for strings that can parse as JSON. For the JSON handling
|
|
// to apply, both old and new must be valid JSON.
|
|
// For single-line strings that don't parse as JSON we just fall
|
|
// out of this switch block and do the default old -> new rendering.
|
|
oldS := old.AsString()
|
|
newS := new.AsString()
|
|
|
|
{
|
|
// Special behavior for JSON strings containing object or
|
|
// list values.
|
|
oldBytes := []byte(oldS)
|
|
newBytes := []byte(newS)
|
|
oldType, oldErr := ctyjson.ImpliedType(oldBytes)
|
|
newType, newErr := ctyjson.ImpliedType(newBytes)
|
|
if oldErr == nil && newErr == nil && !(oldType.IsPrimitiveType() && newType.IsPrimitiveType()) {
|
|
oldJV, oldErr := ctyjson.Unmarshal(oldBytes, oldType)
|
|
newJV, newErr := ctyjson.Unmarshal(newBytes, newType)
|
|
if oldErr == nil && newErr == nil {
|
|
if !oldJV.RawEquals(newJV) { // two JSON values may differ only in insignificant whitespace
|
|
p.buf.WriteString("jsonencode(")
|
|
p.buf.WriteByte('\n')
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
p.writeActionSymbol(plans.Update)
|
|
p.writeValueDiff(oldJV, newJV, indent+4, path)
|
|
p.buf.WriteByte('\n')
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.buf.WriteByte(')')
|
|
} else {
|
|
// if they differ only in insignificant whitespace
|
|
// then we'll note that but still expand out the
|
|
// effective value.
|
|
if p.pathForcesNewResource(path) {
|
|
p.buf.WriteString(p.color.Color("jsonencode( [red]# whitespace changes force replacement[reset]"))
|
|
} else {
|
|
p.buf.WriteString(p.color.Color("jsonencode( [dim]# whitespace changes[reset]"))
|
|
}
|
|
p.buf.WriteByte('\n')
|
|
p.buf.WriteString(strings.Repeat(" ", indent+4))
|
|
p.writeValue(oldJV, plans.NoOp, indent+4)
|
|
p.buf.WriteByte('\n')
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.buf.WriteByte(')')
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if !strings.Contains(oldS, "\n") && !strings.Contains(newS, "\n") {
|
|
break
|
|
}
|
|
|
|
p.buf.WriteString("<<-EOT")
|
|
if p.pathForcesNewResource(path) {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
p.buf.WriteString("\n")
|
|
|
|
var oldLines, newLines []cty.Value
|
|
{
|
|
r := strings.NewReader(oldS)
|
|
sc := bufio.NewScanner(r)
|
|
for sc.Scan() {
|
|
oldLines = append(oldLines, cty.StringVal(sc.Text()))
|
|
}
|
|
}
|
|
{
|
|
r := strings.NewReader(newS)
|
|
sc := bufio.NewScanner(r)
|
|
for sc.Scan() {
|
|
newLines = append(newLines, cty.StringVal(sc.Text()))
|
|
}
|
|
}
|
|
|
|
// Optimization for strings which are exactly equal: just print
|
|
// directly without calculating the sequence diff. This makes a
|
|
// significant difference when this code path is reached via a
|
|
// writeValue call with a large multi-line string.
|
|
if oldS == newS {
|
|
for _, line := range newLines {
|
|
p.buf.WriteString(strings.Repeat(" ", indent+4))
|
|
p.buf.WriteString(line.AsString())
|
|
p.buf.WriteString("\n")
|
|
}
|
|
} else {
|
|
diffLines := ctySequenceDiff(oldLines, newLines)
|
|
for _, diffLine := range diffLines {
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
p.writeActionSymbol(diffLine.Action)
|
|
|
|
switch diffLine.Action {
|
|
case plans.NoOp, plans.Delete:
|
|
p.buf.WriteString(diffLine.Before.AsString())
|
|
case plans.Create:
|
|
p.buf.WriteString(diffLine.After.AsString())
|
|
default:
|
|
// Should never happen since the above covers all
|
|
// actions that ctySequenceDiff can return for strings
|
|
p.buf.WriteString(diffLine.After.AsString())
|
|
|
|
}
|
|
p.buf.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
p.buf.WriteString(strings.Repeat(" ", indent)) // +4 here because there's no symbol
|
|
p.buf.WriteString("EOT")
|
|
|
|
return
|
|
|
|
case ty.IsSetType():
|
|
p.buf.WriteString("[")
|
|
if p.pathForcesNewResource(path) {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
p.buf.WriteString("\n")
|
|
|
|
var addedVals, removedVals, allVals []cty.Value
|
|
for it := old.ElementIterator(); it.Next(); {
|
|
_, val := it.Element()
|
|
allVals = append(allVals, val)
|
|
if new.HasElement(val).False() {
|
|
removedVals = append(removedVals, val)
|
|
}
|
|
}
|
|
for it := new.ElementIterator(); it.Next(); {
|
|
_, val := it.Element()
|
|
allVals = append(allVals, val)
|
|
if val.IsKnown() && old.HasElement(val).False() {
|
|
addedVals = append(addedVals, val)
|
|
}
|
|
}
|
|
|
|
var all, added, removed cty.Value
|
|
if len(allVals) > 0 {
|
|
all = cty.SetVal(allVals)
|
|
} else {
|
|
all = cty.SetValEmpty(ty.ElementType())
|
|
}
|
|
if len(addedVals) > 0 {
|
|
added = cty.SetVal(addedVals)
|
|
} else {
|
|
added = cty.SetValEmpty(ty.ElementType())
|
|
}
|
|
if len(removedVals) > 0 {
|
|
removed = cty.SetVal(removedVals)
|
|
} else {
|
|
removed = cty.SetValEmpty(ty.ElementType())
|
|
}
|
|
|
|
suppressedElements := 0
|
|
for it := all.ElementIterator(); it.Next(); {
|
|
_, val := it.Element()
|
|
|
|
var action plans.Action
|
|
switch {
|
|
case !val.IsKnown():
|
|
action = plans.Update
|
|
case added.HasElement(val).True():
|
|
action = plans.Create
|
|
case removed.HasElement(val).True():
|
|
action = plans.Delete
|
|
default:
|
|
action = plans.NoOp
|
|
}
|
|
|
|
if action == plans.NoOp && !p.verbose {
|
|
suppressedElements++
|
|
continue
|
|
}
|
|
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
p.writeActionSymbol(action)
|
|
p.writeValue(val, action, indent+4)
|
|
p.buf.WriteString(",\n")
|
|
}
|
|
|
|
if suppressedElements > 0 {
|
|
p.writeActionSymbol(plans.NoOp)
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
noun := "elements"
|
|
if suppressedElements == 1 {
|
|
noun = "element"
|
|
}
|
|
p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun))
|
|
p.buf.WriteString("\n")
|
|
}
|
|
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.buf.WriteString("]")
|
|
return
|
|
case ty.IsListType() || ty.IsTupleType():
|
|
p.buf.WriteString("[")
|
|
if p.pathForcesNewResource(path) {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
p.buf.WriteString("\n")
|
|
|
|
elemDiffs := ctySequenceDiff(old.AsValueSlice(), new.AsValueSlice())
|
|
|
|
// Maintain a stack of suppressed lines in the diff for later
|
|
// display or elision
|
|
var suppressedElements []*plans.Change
|
|
var changeShown bool
|
|
|
|
for i := 0; i < len(elemDiffs); i++ {
|
|
if !p.verbose {
|
|
for i < len(elemDiffs) && elemDiffs[i].Action == plans.NoOp {
|
|
suppressedElements = append(suppressedElements, elemDiffs[i])
|
|
i++
|
|
}
|
|
}
|
|
|
|
// If we have some suppressed elements on the stack…
|
|
if len(suppressedElements) > 0 {
|
|
// If we've just rendered a change, display the first
|
|
// element in the stack as context
|
|
if changeShown {
|
|
elemDiff := suppressedElements[0]
|
|
p.buf.WriteString(strings.Repeat(" ", indent+4))
|
|
p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
|
|
p.buf.WriteString(",\n")
|
|
suppressedElements = suppressedElements[1:]
|
|
}
|
|
|
|
hidden := len(suppressedElements)
|
|
|
|
// If we're not yet at the end of the list, capture the
|
|
// last element on the stack as context for the upcoming
|
|
// change to be rendered
|
|
var nextContextDiff *plans.Change
|
|
if hidden > 0 && i < len(elemDiffs) {
|
|
hidden--
|
|
nextContextDiff = suppressedElements[hidden]
|
|
}
|
|
|
|
// If there are still hidden elements, show an elision
|
|
// statement counting them
|
|
if hidden > 0 {
|
|
p.writeActionSymbol(plans.NoOp)
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
noun := "elements"
|
|
if hidden == 1 {
|
|
noun = "element"
|
|
}
|
|
p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), hidden, noun))
|
|
p.buf.WriteString("\n")
|
|
}
|
|
|
|
// Display the next context diff if it was captured above
|
|
if nextContextDiff != nil {
|
|
p.buf.WriteString(strings.Repeat(" ", indent+4))
|
|
p.writeValue(nextContextDiff.After, nextContextDiff.Action, indent+4)
|
|
p.buf.WriteString(",\n")
|
|
}
|
|
|
|
// Suppressed elements have now been handled so clear them again
|
|
suppressedElements = nil
|
|
}
|
|
|
|
if i >= len(elemDiffs) {
|
|
break
|
|
}
|
|
|
|
elemDiff := elemDiffs[i]
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
p.writeActionSymbol(elemDiff.Action)
|
|
switch elemDiff.Action {
|
|
case plans.NoOp, plans.Delete:
|
|
p.writeValue(elemDiff.Before, elemDiff.Action, indent+4)
|
|
case plans.Update:
|
|
p.writeValueDiff(elemDiff.Before, elemDiff.After, indent+4, path)
|
|
case plans.Create:
|
|
p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
|
|
default:
|
|
// Should never happen since the above covers all
|
|
// actions that ctySequenceDiff can return.
|
|
p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
|
|
}
|
|
|
|
p.buf.WriteString(",\n")
|
|
changeShown = true
|
|
}
|
|
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.buf.WriteString("]")
|
|
|
|
return
|
|
|
|
case ty.IsMapType():
|
|
p.buf.WriteString("{")
|
|
if p.pathForcesNewResource(path) {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
p.buf.WriteString("\n")
|
|
|
|
var allKeys []string
|
|
keyLen := 0
|
|
for it := old.ElementIterator(); it.Next(); {
|
|
k, _ := it.Element()
|
|
keyStr := k.AsString()
|
|
allKeys = append(allKeys, keyStr)
|
|
if len(keyStr) > keyLen {
|
|
keyLen = len(keyStr)
|
|
}
|
|
}
|
|
for it := new.ElementIterator(); it.Next(); {
|
|
k, _ := it.Element()
|
|
keyStr := k.AsString()
|
|
allKeys = append(allKeys, keyStr)
|
|
if len(keyStr) > keyLen {
|
|
keyLen = len(keyStr)
|
|
}
|
|
}
|
|
|
|
sort.Strings(allKeys)
|
|
|
|
suppressedElements := 0
|
|
lastK := ""
|
|
for i, k := range allKeys {
|
|
if i > 0 && lastK == k {
|
|
continue // skip duplicates (list is sorted)
|
|
}
|
|
lastK = k
|
|
|
|
kV := cty.StringVal(k)
|
|
var action plans.Action
|
|
if old.HasIndex(kV).False() {
|
|
action = plans.Create
|
|
} else if new.HasIndex(kV).False() {
|
|
action = plans.Delete
|
|
}
|
|
|
|
if old.HasIndex(kV).True() && new.HasIndex(kV).True() {
|
|
if ctyEqualValueAndMarks(old.Index(kV), new.Index(kV)) {
|
|
action = plans.NoOp
|
|
} else {
|
|
action = plans.Update
|
|
}
|
|
}
|
|
|
|
if action == plans.NoOp && !p.verbose {
|
|
suppressedElements++
|
|
continue
|
|
}
|
|
|
|
path := append(path, cty.IndexStep{Key: kV})
|
|
|
|
oldV := old.Index(kV)
|
|
newV := new.Index(kV)
|
|
p.writeSensitivityWarning(oldV, newV, indent+2, action, false)
|
|
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
p.writeActionSymbol(action)
|
|
p.writeValue(kV, action, indent+4)
|
|
p.buf.WriteString(strings.Repeat(" ", keyLen-len(k)))
|
|
p.buf.WriteString(" = ")
|
|
switch action {
|
|
case plans.Create, plans.NoOp:
|
|
v := new.Index(kV)
|
|
if v.HasMark(marks.Sensitive) {
|
|
p.buf.WriteString("(sensitive)")
|
|
} else {
|
|
p.writeValue(v, action, indent+4)
|
|
}
|
|
case plans.Delete:
|
|
oldV := old.Index(kV)
|
|
newV := cty.NullVal(oldV.Type())
|
|
p.writeValueDiff(oldV, newV, indent+4, path)
|
|
default:
|
|
if oldV.HasMark(marks.Sensitive) || newV.HasMark(marks.Sensitive) {
|
|
p.buf.WriteString("(sensitive)")
|
|
} else {
|
|
p.writeValueDiff(oldV, newV, indent+4, path)
|
|
}
|
|
}
|
|
|
|
p.buf.WriteByte('\n')
|
|
}
|
|
|
|
if suppressedElements > 0 {
|
|
p.writeActionSymbol(plans.NoOp)
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
noun := "elements"
|
|
if suppressedElements == 1 {
|
|
noun = "element"
|
|
}
|
|
p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun))
|
|
p.buf.WriteString("\n")
|
|
}
|
|
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.buf.WriteString("}")
|
|
|
|
return
|
|
case ty.IsObjectType():
|
|
p.buf.WriteString("{")
|
|
p.buf.WriteString("\n")
|
|
|
|
forcesNewResource := p.pathForcesNewResource(path)
|
|
|
|
var allKeys []string
|
|
keyLen := 0
|
|
for it := old.ElementIterator(); it.Next(); {
|
|
k, _ := it.Element()
|
|
keyStr := k.AsString()
|
|
allKeys = append(allKeys, keyStr)
|
|
if len(keyStr) > keyLen {
|
|
keyLen = len(keyStr)
|
|
}
|
|
}
|
|
for it := new.ElementIterator(); it.Next(); {
|
|
k, _ := it.Element()
|
|
keyStr := k.AsString()
|
|
allKeys = append(allKeys, keyStr)
|
|
if len(keyStr) > keyLen {
|
|
keyLen = len(keyStr)
|
|
}
|
|
}
|
|
|
|
sort.Strings(allKeys)
|
|
|
|
suppressedElements := 0
|
|
lastK := ""
|
|
for i, k := range allKeys {
|
|
if i > 0 && lastK == k {
|
|
continue // skip duplicates (list is sorted)
|
|
}
|
|
lastK = k
|
|
|
|
kV := k
|
|
var action plans.Action
|
|
if !old.Type().HasAttribute(kV) {
|
|
action = plans.Create
|
|
} else if !new.Type().HasAttribute(kV) {
|
|
action = plans.Delete
|
|
} else if ctyEqualValueAndMarks(old.GetAttr(kV), new.GetAttr(kV)) {
|
|
action = plans.NoOp
|
|
} else {
|
|
action = plans.Update
|
|
}
|
|
|
|
if action == plans.NoOp && !p.verbose {
|
|
suppressedElements++
|
|
continue
|
|
}
|
|
|
|
path := append(path, cty.GetAttrStep{Name: kV})
|
|
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
p.writeActionSymbol(action)
|
|
p.buf.WriteString(k)
|
|
p.buf.WriteString(strings.Repeat(" ", keyLen-len(k)))
|
|
p.buf.WriteString(" = ")
|
|
|
|
switch action {
|
|
case plans.Create, plans.NoOp:
|
|
v := new.GetAttr(kV)
|
|
p.writeValue(v, action, indent+4)
|
|
case plans.Delete:
|
|
oldV := old.GetAttr(kV)
|
|
newV := cty.NullVal(oldV.Type())
|
|
p.writeValueDiff(oldV, newV, indent+4, path)
|
|
default:
|
|
oldV := old.GetAttr(kV)
|
|
newV := new.GetAttr(kV)
|
|
p.writeValueDiff(oldV, newV, indent+4, path)
|
|
}
|
|
|
|
p.buf.WriteString("\n")
|
|
}
|
|
|
|
if suppressedElements > 0 {
|
|
p.writeActionSymbol(plans.NoOp)
|
|
p.buf.WriteString(strings.Repeat(" ", indent+2))
|
|
noun := "elements"
|
|
if suppressedElements == 1 {
|
|
noun = "element"
|
|
}
|
|
p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun))
|
|
p.buf.WriteString("\n")
|
|
}
|
|
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.buf.WriteString("}")
|
|
|
|
if forcesNewResource {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
// In all other cases, we just show the new and old values as-is
|
|
p.writeValue(old, plans.Delete, indent)
|
|
if new.IsNull() {
|
|
p.buf.WriteString(p.color.Color(" [dark_gray]->[reset] "))
|
|
} else {
|
|
p.buf.WriteString(p.color.Color(" [yellow]->[reset] "))
|
|
}
|
|
|
|
p.writeValue(new, plans.Create, indent)
|
|
if p.pathForcesNewResource(path) {
|
|
p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
|
|
}
|
|
}
|
|
|
|
// writeActionSymbol writes a symbol to represent the given action, followed
|
|
// by a space.
|
|
//
|
|
// It only supports the actions that can be represented with a single character:
|
|
// Create, Delete, Update and NoAction.
|
|
func (p *blockBodyDiffPrinter) writeActionSymbol(action plans.Action) {
|
|
switch action {
|
|
case plans.Create:
|
|
p.buf.WriteString(p.color.Color("[green]+[reset] "))
|
|
case plans.Delete:
|
|
p.buf.WriteString(p.color.Color("[red]-[reset] "))
|
|
case plans.Update:
|
|
p.buf.WriteString(p.color.Color("[yellow]~[reset] "))
|
|
case plans.NoOp:
|
|
p.buf.WriteString(" ")
|
|
default:
|
|
// Should never happen
|
|
p.buf.WriteString(p.color.Color("? "))
|
|
}
|
|
}
|
|
|
|
func (p *blockBodyDiffPrinter) writeSensitivityWarning(old, new cty.Value, indent int, action plans.Action, isBlock bool) {
|
|
// Dont' show this warning for create or delete
|
|
if action == plans.Create || action == plans.Delete {
|
|
return
|
|
}
|
|
|
|
// Customize the warning based on if it is an attribute or block
|
|
diffType := "attribute value"
|
|
if isBlock {
|
|
diffType = "block"
|
|
}
|
|
|
|
// If only attribute sensitivity is changing, clarify that the value is unchanged
|
|
var valueUnchangedSuffix string
|
|
if !isBlock {
|
|
oldUnmarked, _ := old.UnmarkDeep()
|
|
newUnmarked, _ := new.UnmarkDeep()
|
|
if oldUnmarked.RawEquals(newUnmarked) {
|
|
valueUnchangedSuffix = " The value is unchanged."
|
|
}
|
|
}
|
|
|
|
if new.HasMark(marks.Sensitive) && !old.HasMark(marks.Sensitive) {
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.buf.WriteString(fmt.Sprintf(p.color.Color("# [yellow]Warning:[reset] this %s will be marked as sensitive and will not\n"), diffType))
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.buf.WriteString(fmt.Sprintf("# display in UI output after applying this change.%s\n", valueUnchangedSuffix))
|
|
}
|
|
|
|
// Note if changing this attribute will change its sensitivity
|
|
if old.HasMark(marks.Sensitive) && !new.HasMark(marks.Sensitive) {
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.buf.WriteString(fmt.Sprintf(p.color.Color("# [yellow]Warning:[reset] this %s will no longer be marked as sensitive\n"), diffType))
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.buf.WriteString(fmt.Sprintf("# after applying this change.%s\n", valueUnchangedSuffix))
|
|
}
|
|
}
|
|
|
|
func (p *blockBodyDiffPrinter) pathForcesNewResource(path cty.Path) bool {
|
|
if !p.action.IsReplace() || p.requiredReplace.Empty() {
|
|
// "requiredReplace" only applies when the instance is being replaced,
|
|
// and we should only inspect that set if it is not empty
|
|
return false
|
|
}
|
|
return p.requiredReplace.Has(path)
|
|
}
|
|
|
|
func ctyEmptyString(value cty.Value) bool {
|
|
if !value.IsNull() && value.IsKnown() {
|
|
valueType := value.Type()
|
|
if valueType == cty.String && value.AsString() == "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func ctyGetAttrMaybeNull(val cty.Value, name string) cty.Value {
|
|
attrType := val.Type().AttributeType(name)
|
|
|
|
if val.IsNull() {
|
|
return cty.NullVal(attrType)
|
|
}
|
|
|
|
// We treat "" as null here
|
|
// as existing SDK doesn't support null yet.
|
|
// This allows us to avoid spurious diffs
|
|
// until we introduce null to the SDK.
|
|
attrValue := val.GetAttr(name)
|
|
// If the value is marked, the ctyEmptyString function will fail
|
|
if !val.ContainsMarked() && ctyEmptyString(attrValue) {
|
|
return cty.NullVal(attrType)
|
|
}
|
|
|
|
return attrValue
|
|
}
|
|
|
|
func ctyCollectionValues(val cty.Value) []cty.Value {
|
|
if !val.IsKnown() || val.IsNull() {
|
|
return nil
|
|
}
|
|
|
|
ret := make([]cty.Value, 0, val.LengthInt())
|
|
for it := val.ElementIterator(); it.Next(); {
|
|
_, value := it.Element()
|
|
ret = append(ret, value)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// ctySequenceDiff returns differences between given sequences of cty.Value(s)
|
|
// in the form of Create, Delete, or Update actions (for objects).
|
|
func ctySequenceDiff(old, new []cty.Value) []*plans.Change {
|
|
var ret []*plans.Change
|
|
lcs := objchange.LongestCommonSubsequence(old, new)
|
|
var oldI, newI, lcsI int
|
|
for oldI < len(old) || newI < len(new) || lcsI < len(lcs) {
|
|
// We first process items in the old and new sequences which are not
|
|
// equal to the current common sequence item. Old items are marked as
|
|
// deletions, and new items are marked as additions.
|
|
//
|
|
// There is an exception for deleted & created object items, which we
|
|
// try to render as updates where that makes sense.
|
|
for oldI < len(old) && (lcsI >= len(lcs) || !old[oldI].RawEquals(lcs[lcsI])) {
|
|
// Render this as an object update if all of these are true:
|
|
//
|
|
// - the current old item is an object;
|
|
// - there's a current new item which is also an object;
|
|
// - either there are no common items left, or the current new item
|
|
// doesn't equal the current common item.
|
|
//
|
|
// Why do we need the the last clause? If we have current items in all
|
|
// three sequences, and the current new item is equal to a common item,
|
|
// then we should just need to advance the old item list and we'll
|
|
// eventually find a common item matching both old and new.
|
|
//
|
|
// This combination of conditions allows us to render an object update
|
|
// diff instead of a combination of delete old & create new.
|
|
isObjectDiff := old[oldI].Type().IsObjectType() && newI < len(new) && new[newI].Type().IsObjectType() && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI]))
|
|
if isObjectDiff {
|
|
ret = append(ret, &plans.Change{
|
|
Action: plans.Update,
|
|
Before: old[oldI],
|
|
After: new[newI],
|
|
})
|
|
oldI++
|
|
newI++ // we also consume the next "new" in this case
|
|
continue
|
|
}
|
|
|
|
// Otherwise, this item is not part of the common sequence, so
|
|
// render as a deletion.
|
|
ret = append(ret, &plans.Change{
|
|
Action: plans.Delete,
|
|
Before: old[oldI],
|
|
After: cty.NullVal(old[oldI].Type()),
|
|
})
|
|
oldI++
|
|
}
|
|
for newI < len(new) && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI])) {
|
|
ret = append(ret, &plans.Change{
|
|
Action: plans.Create,
|
|
Before: cty.NullVal(new[newI].Type()),
|
|
After: new[newI],
|
|
})
|
|
newI++
|
|
}
|
|
|
|
// When we've exhausted the old & new sequences of items which are not
|
|
// in the common subsequence, we render a common item and continue.
|
|
if lcsI < len(lcs) {
|
|
ret = append(ret, &plans.Change{
|
|
Action: plans.NoOp,
|
|
Before: lcs[lcsI],
|
|
After: lcs[lcsI],
|
|
})
|
|
|
|
// All of our indexes advance together now, since the line
|
|
// is common to all three sequences.
|
|
lcsI++
|
|
oldI++
|
|
newI++
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// ctyEqualValueAndMarks checks equality of two possibly-marked values,
|
|
// considering partially-unknown values and equal values with different marks
|
|
// as inequal
|
|
func ctyEqualWithUnknown(old, new cty.Value) bool {
|
|
if !old.IsWhollyKnown() || !new.IsWhollyKnown() {
|
|
return false
|
|
}
|
|
return ctyEqualValueAndMarks(old, new)
|
|
}
|
|
|
|
// ctyEqualValueAndMarks checks equality of two possibly-marked values,
|
|
// considering equal values with different marks as inequal
|
|
func ctyEqualValueAndMarks(old, new cty.Value) bool {
|
|
oldUnmarked, oldMarks := old.UnmarkDeep()
|
|
newUnmarked, newMarks := new.UnmarkDeep()
|
|
sameValue := oldUnmarked.Equals(newUnmarked)
|
|
return sameValue.IsKnown() && sameValue.True() && oldMarks.Equal(newMarks)
|
|
}
|
|
|
|
// ctyTypesEqual checks equality of two types more loosely
|
|
// by avoiding checks of object/tuple elements
|
|
// as we render differences on element-by-element basis anyway
|
|
func ctyTypesEqual(oldT, newT cty.Type) bool {
|
|
if oldT.IsObjectType() && newT.IsObjectType() {
|
|
return true
|
|
}
|
|
if oldT.IsTupleType() && newT.IsTupleType() {
|
|
return true
|
|
}
|
|
return oldT.Equals(newT)
|
|
}
|
|
|
|
func ctyEnsurePathCapacity(path cty.Path, minExtra int) cty.Path {
|
|
if cap(path)-len(path) >= minExtra {
|
|
return path
|
|
}
|
|
newCap := cap(path) * 2
|
|
if newCap < (len(path) + minExtra) {
|
|
newCap = len(path) + minExtra
|
|
}
|
|
newPath := make(cty.Path, len(path), newCap)
|
|
copy(newPath, path)
|
|
return newPath
|
|
}
|
|
|
|
// ctyNullBlockListAsEmpty either returns the given value verbatim if it is non-nil
|
|
// or returns an empty value of a suitable type to serve as a placeholder for it.
|
|
//
|
|
// In particular, this function handles the special situation where a "list" is
|
|
// actually represented as a tuple type where nested blocks contain
|
|
// dynamically-typed values.
|
|
func ctyNullBlockListAsEmpty(in cty.Value) cty.Value {
|
|
if !in.IsNull() {
|
|
return in
|
|
}
|
|
if ty := in.Type(); ty.IsListType() {
|
|
return cty.ListValEmpty(ty.ElementType())
|
|
}
|
|
return cty.EmptyTupleVal // must need a tuple, then
|
|
}
|
|
|
|
// ctyNullBlockMapAsEmpty either returns the given value verbatim if it is non-nil
|
|
// or returns an empty value of a suitable type to serve as a placeholder for it.
|
|
//
|
|
// In particular, this function handles the special situation where a "map" is
|
|
// actually represented as an object type where nested blocks contain
|
|
// dynamically-typed values.
|
|
func ctyNullBlockMapAsEmpty(in cty.Value) cty.Value {
|
|
if !in.IsNull() {
|
|
return in
|
|
}
|
|
if ty := in.Type(); ty.IsMapType() {
|
|
return cty.MapValEmpty(ty.ElementType())
|
|
}
|
|
return cty.EmptyObjectVal // must need an object, then
|
|
}
|
|
|
|
// ctyNullBlockSetAsEmpty either returns the given value verbatim if it is non-nil
|
|
// or returns an empty value of a suitable type to serve as a placeholder for it.
|
|
func ctyNullBlockSetAsEmpty(in cty.Value) cty.Value {
|
|
if !in.IsNull() {
|
|
return in
|
|
}
|
|
// Dynamically-typed attributes are not supported inside blocks backed by
|
|
// sets, so our result here is always a set.
|
|
return cty.SetValEmpty(in.Type().ElementType())
|
|
}
|
|
|
|
// DiffActionSymbol returns a string that, once passed through a
|
|
// colorstring.Colorize, will produce a result that can be written
|
|
// to a terminal to produce a symbol made of three printable
|
|
// characters, possibly interspersed with VT100 color codes.
|
|
func DiffActionSymbol(action plans.Action) string {
|
|
switch action {
|
|
case plans.DeleteThenCreate:
|
|
return "[red]-[reset]/[green]+[reset]"
|
|
case plans.CreateThenDelete:
|
|
return "[green]+[reset]/[red]-[reset]"
|
|
case plans.Create:
|
|
return " [green]+[reset]"
|
|
case plans.Delete:
|
|
return " [red]-[reset]"
|
|
case plans.Read:
|
|
return " [cyan]<=[reset]"
|
|
case plans.Update:
|
|
return " [yellow]~[reset]"
|
|
default:
|
|
return " ?"
|
|
}
|
|
}
|
|
|
|
// Extremely coarse heuristic for determining whether or not a given attribute
|
|
// name is important for identifying a resource. In the future, this may be
|
|
// replaced by a flag in the schema, but for now this is likely to be good
|
|
// enough.
|
|
func identifyingAttribute(name string, attrSchema *configschema.Attribute) bool {
|
|
return name == "id" || name == "tags" || name == "name"
|
|
}
|
|
|
|
func (p *blockBodyDiffPrinter) writeSkippedAttr(skipped, indent int) {
|
|
if skipped > 0 {
|
|
noun := "attributes"
|
|
if skipped == 1 {
|
|
noun = "attribute"
|
|
}
|
|
p.buf.WriteString("\n")
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), skipped, noun))
|
|
}
|
|
}
|
|
|
|
func (p *blockBodyDiffPrinter) writeSkippedElems(skipped, indent int) {
|
|
if skipped > 0 {
|
|
noun := "elements"
|
|
if skipped == 1 {
|
|
noun = "element"
|
|
}
|
|
p.buf.WriteString(strings.Repeat(" ", indent))
|
|
p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), skipped, noun))
|
|
p.buf.WriteString("\n")
|
|
}
|
|
}
|