github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/command/format/diff.go (about)

     1  package format
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"log"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/hashicorp/hcl/v2/hclsyntax"
    12  	"github.com/mitchellh/colorstring"
    13  	"github.com/zclconf/go-cty/cty"
    14  	ctyjson "github.com/zclconf/go-cty/cty/json"
    15  
    16  	"github.com/eliastor/durgaform/internal/addrs"
    17  	"github.com/eliastor/durgaform/internal/configs/configschema"
    18  	"github.com/eliastor/durgaform/internal/lang/marks"
    19  	"github.com/eliastor/durgaform/internal/plans"
    20  	"github.com/eliastor/durgaform/internal/plans/objchange"
    21  	"github.com/eliastor/durgaform/internal/states"
    22  )
    23  
    24  // DiffLanguage controls the description of the resource change reasons.
    25  type DiffLanguage rune
    26  
    27  //go:generate go run golang.org/x/tools/cmd/stringer -type=DiffLanguage diff.go
    28  
    29  const (
    30  	// DiffLanguageProposedChange indicates that the change is one which is
    31  	// planned to be applied.
    32  	DiffLanguageProposedChange DiffLanguage = 'P'
    33  
    34  	// DiffLanguageDetectedDrift indicates that the change is detected drift
    35  	// from the configuration.
    36  	DiffLanguageDetectedDrift DiffLanguage = 'D'
    37  )
    38  
    39  // ResourceChange returns a string representation of a change to a particular
    40  // resource, for inclusion in user-facing plan output.
    41  //
    42  // The resource schema must be provided along with the change so that the
    43  // formatted change can reflect the configuration structure for the associated
    44  // resource.
    45  //
    46  // If "color" is non-nil, it will be used to color the result. Otherwise,
    47  // no color codes will be included.
    48  func ResourceChange(
    49  	change *plans.ResourceInstanceChange,
    50  	schema *configschema.Block,
    51  	color *colorstring.Colorize,
    52  	language DiffLanguage,
    53  ) string {
    54  	addr := change.Addr
    55  	var buf bytes.Buffer
    56  
    57  	if color == nil {
    58  		color = &colorstring.Colorize{
    59  			Colors:  colorstring.DefaultColors,
    60  			Disable: true,
    61  			Reset:   false,
    62  		}
    63  	}
    64  
    65  	dispAddr := addr.String()
    66  	if change.DeposedKey != states.NotDeposed {
    67  		dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, change.DeposedKey)
    68  	}
    69  
    70  	switch change.Action {
    71  	case plans.Create:
    72  		buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] will be created"), dispAddr))
    73  	case plans.Read:
    74  		buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] will be read during apply"), dispAddr))
    75  		switch change.ActionReason {
    76  		case plans.ResourceInstanceReadBecauseConfigUnknown:
    77  			buf.WriteString("\n  # (config refers to values not yet known)")
    78  		case plans.ResourceInstanceReadBecauseDependencyPending:
    79  			buf.WriteString("\n  # (depends on a resource or a module with changes pending)")
    80  		}
    81  	case plans.Update:
    82  		switch language {
    83  		case DiffLanguageProposedChange:
    84  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] will be updated in-place"), dispAddr))
    85  		case DiffLanguageDetectedDrift:
    86  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] has changed"), dispAddr))
    87  		default:
    88  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] update (unknown reason %s)"), dispAddr, language))
    89  		}
    90  	case plans.CreateThenDelete, plans.DeleteThenCreate:
    91  		switch change.ActionReason {
    92  		case plans.ResourceInstanceReplaceBecauseTainted:
    93  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] is tainted, so must be [bold][red]replaced"), dispAddr))
    94  		case plans.ResourceInstanceReplaceByRequest:
    95  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] will be [bold][red]replaced[reset], as requested"), dispAddr))
    96  		case plans.ResourceInstanceReplaceByTriggers:
    97  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] will be [bold][red]replaced[reset] due to changes in replace_triggered_by"), dispAddr))
    98  		default:
    99  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] must be [bold][red]replaced"), dispAddr))
   100  		}
   101  	case plans.Delete:
   102  		switch language {
   103  		case DiffLanguageProposedChange:
   104  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] will be [bold][red]destroyed"), dispAddr))
   105  		case DiffLanguageDetectedDrift:
   106  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] has been deleted"), dispAddr))
   107  		default:
   108  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] delete (unknown reason %s)"), dispAddr, language))
   109  		}
   110  		// We can sometimes give some additional detail about why we're
   111  		// proposing to delete. We show this as additional notes, rather than
   112  		// as additional wording in the main action statement, in an attempt
   113  		// to make the "will be destroyed" message prominent and consistent
   114  		// in all cases, for easier scanning of this often-risky action.
   115  		switch change.ActionReason {
   116  		case plans.ResourceInstanceDeleteBecauseNoResourceConfig:
   117  			buf.WriteString(fmt.Sprintf("\n  # (because %s is not in configuration)", addr.Resource.Resource))
   118  		case plans.ResourceInstanceDeleteBecauseNoModule:
   119  			// FIXME: Ideally we'd truncate addr.Module to reflect the earliest
   120  			// step that doesn't exist, so it's clearer which call this refers
   121  			// to, but we don't have enough information out here in the UI layer
   122  			// to decide that; only the "expander" in Durgaform Core knows
   123  			// which module instance keys are actually declared.
   124  			buf.WriteString(fmt.Sprintf("\n  # (because %s is not in configuration)", addr.Module))
   125  		case plans.ResourceInstanceDeleteBecauseWrongRepetition:
   126  			// We have some different variations of this one
   127  			switch addr.Resource.Key.(type) {
   128  			case nil:
   129  				buf.WriteString("\n  # (because resource uses count or for_each)")
   130  			case addrs.IntKey:
   131  				buf.WriteString("\n  # (because resource does not use count)")
   132  			case addrs.StringKey:
   133  				buf.WriteString("\n  # (because resource does not use for_each)")
   134  			}
   135  		case plans.ResourceInstanceDeleteBecauseCountIndex:
   136  			buf.WriteString(fmt.Sprintf("\n  # (because index %s is out of range for count)", addr.Resource.Key))
   137  		case plans.ResourceInstanceDeleteBecauseEachKey:
   138  			buf.WriteString(fmt.Sprintf("\n  # (because key %s is not in for_each map)", addr.Resource.Key))
   139  		}
   140  		if change.DeposedKey != states.NotDeposed {
   141  			// Some extra context about this unusual situation.
   142  			buf.WriteString(color.Color("\n  # (left over from a partially-failed replacement of this instance)"))
   143  		}
   144  	case plans.NoOp:
   145  		if change.Moved() {
   146  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] has moved to [bold]%s[reset]"), change.PrevRunAddr.String(), dispAddr))
   147  			break
   148  		}
   149  		fallthrough
   150  	default:
   151  		// should never happen, since the above is exhaustive
   152  		buf.WriteString(fmt.Sprintf("%s has an action the plan renderer doesn't support (this is a bug)", dispAddr))
   153  	}
   154  	buf.WriteString(color.Color("[reset]\n"))
   155  
   156  	if change.Moved() && change.Action != plans.NoOp {
   157  		buf.WriteString(fmt.Sprintf(color.Color("  # [reset](moved from %s)\n"), change.PrevRunAddr.String()))
   158  	}
   159  
   160  	if change.Moved() && change.Action == plans.NoOp {
   161  		buf.WriteString("    ")
   162  	} else {
   163  		buf.WriteString(color.Color(DiffActionSymbol(change.Action)) + " ")
   164  	}
   165  
   166  	switch addr.Resource.Resource.Mode {
   167  	case addrs.ManagedResourceMode:
   168  		buf.WriteString(fmt.Sprintf(
   169  			"resource %q %q",
   170  			addr.Resource.Resource.Type,
   171  			addr.Resource.Resource.Name,
   172  		))
   173  	case addrs.DataResourceMode:
   174  		buf.WriteString(fmt.Sprintf(
   175  			"data %q %q",
   176  			addr.Resource.Resource.Type,
   177  			addr.Resource.Resource.Name,
   178  		))
   179  	default:
   180  		// should never happen, since the above is exhaustive
   181  		buf.WriteString(addr.String())
   182  	}
   183  
   184  	buf.WriteString(" {")
   185  
   186  	p := blockBodyDiffPrinter{
   187  		buf:             &buf,
   188  		color:           color,
   189  		action:          change.Action,
   190  		requiredReplace: change.RequiredReplace,
   191  	}
   192  
   193  	// Most commonly-used resources have nested blocks that result in us
   194  	// going at least three traversals deep while we recurse here, so we'll
   195  	// start with that much capacity and then grow as needed for deeper
   196  	// structures.
   197  	path := make(cty.Path, 0, 3)
   198  
   199  	result := p.writeBlockBodyDiff(schema, change.Before, change.After, 6, path)
   200  	if result.bodyWritten {
   201  		buf.WriteString("\n")
   202  		buf.WriteString(strings.Repeat(" ", 4))
   203  	}
   204  	buf.WriteString("}\n")
   205  
   206  	return buf.String()
   207  }
   208  
   209  // OutputChanges returns a string representation of a set of changes to output
   210  // values for inclusion in user-facing plan output.
   211  //
   212  // If "color" is non-nil, it will be used to color the result. Otherwise,
   213  // no color codes will be included.
   214  func OutputChanges(
   215  	changes []*plans.OutputChangeSrc,
   216  	color *colorstring.Colorize,
   217  ) string {
   218  	var buf bytes.Buffer
   219  	p := blockBodyDiffPrinter{
   220  		buf:    &buf,
   221  		color:  color,
   222  		action: plans.Update, // not actually used in this case, because we're not printing a containing block
   223  	}
   224  
   225  	// We're going to reuse the codepath we used for printing resource block
   226  	// diffs, by pretending that the set of defined outputs are the attributes
   227  	// of some resource. It's a little forced to do this, but it gives us all
   228  	// the same formatting heuristics as we normally use for resource
   229  	// attributes.
   230  	oldVals := make(map[string]cty.Value, len(changes))
   231  	newVals := make(map[string]cty.Value, len(changes))
   232  	synthSchema := &configschema.Block{
   233  		Attributes: make(map[string]*configschema.Attribute, len(changes)),
   234  	}
   235  	for _, changeSrc := range changes {
   236  		name := changeSrc.Addr.OutputValue.Name
   237  		change, err := changeSrc.Decode()
   238  		if err != nil {
   239  			// It'd be weird to get a decoding error here because that would
   240  			// suggest that Durgaform itself just produced an invalid plan, and
   241  			// we don't have any good way to ignore it in this codepath, so
   242  			// we'll just log it and ignore it.
   243  			log.Printf("[ERROR] format.OutputChanges: Failed to decode planned change for output %q: %s", name, err)
   244  			continue
   245  		}
   246  		synthSchema.Attributes[name] = &configschema.Attribute{
   247  			Type:      cty.DynamicPseudoType, // output types are decided dynamically based on the given value
   248  			Optional:  true,
   249  			Sensitive: change.Sensitive,
   250  		}
   251  		oldVals[name] = change.Before
   252  		newVals[name] = change.After
   253  	}
   254  
   255  	p.writeBlockBodyDiff(synthSchema, cty.ObjectVal(oldVals), cty.ObjectVal(newVals), 2, nil)
   256  
   257  	return buf.String()
   258  }
   259  
   260  type blockBodyDiffPrinter struct {
   261  	buf             *bytes.Buffer
   262  	color           *colorstring.Colorize
   263  	action          plans.Action
   264  	requiredReplace cty.PathSet
   265  	// verbose is set to true when using the "diff" printer to format state
   266  	verbose bool
   267  }
   268  
   269  type blockBodyDiffResult struct {
   270  	bodyWritten       bool
   271  	skippedAttributes int
   272  	skippedBlocks     int
   273  }
   274  
   275  const forcesNewResourceCaption = " [red]# forces replacement[reset]"
   276  
   277  // writeBlockBodyDiff writes attribute or block differences
   278  // and returns true if any differences were found and written
   279  func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, old, new cty.Value, indent int, path cty.Path) blockBodyDiffResult {
   280  	path = ctyEnsurePathCapacity(path, 1)
   281  	result := blockBodyDiffResult{}
   282  
   283  	// write the attributes diff
   284  	blankBeforeBlocks := p.writeAttrsDiff(schema.Attributes, old, new, indent, path, &result)
   285  	p.writeSkippedAttr(result.skippedAttributes, indent+2)
   286  
   287  	{
   288  		blockTypeNames := make([]string, 0, len(schema.BlockTypes))
   289  		for name := range schema.BlockTypes {
   290  			blockTypeNames = append(blockTypeNames, name)
   291  		}
   292  		sort.Strings(blockTypeNames)
   293  
   294  		for _, name := range blockTypeNames {
   295  			blockS := schema.BlockTypes[name]
   296  			oldVal := ctyGetAttrMaybeNull(old, name)
   297  			newVal := ctyGetAttrMaybeNull(new, name)
   298  
   299  			result.bodyWritten = true
   300  			skippedBlocks := p.writeNestedBlockDiffs(name, blockS, oldVal, newVal, blankBeforeBlocks, indent, path)
   301  			if skippedBlocks > 0 {
   302  				result.skippedBlocks += skippedBlocks
   303  			}
   304  
   305  			// Always include a blank for any subsequent block types.
   306  			blankBeforeBlocks = true
   307  		}
   308  		if result.skippedBlocks > 0 {
   309  			noun := "blocks"
   310  			if result.skippedBlocks == 1 {
   311  				noun = "block"
   312  			}
   313  			p.buf.WriteString("\n\n")
   314  			p.buf.WriteString(strings.Repeat(" ", indent+2))
   315  			p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), result.skippedBlocks, noun))
   316  		}
   317  	}
   318  
   319  	return result
   320  }
   321  
   322  func (p *blockBodyDiffPrinter) writeAttrsDiff(
   323  	attrsS map[string]*configschema.Attribute,
   324  	old, new cty.Value,
   325  	indent int,
   326  	path cty.Path,
   327  	result *blockBodyDiffResult) bool {
   328  
   329  	attrNames := make([]string, 0, len(attrsS))
   330  	displayAttrNames := make(map[string]string, len(attrsS))
   331  	attrNameLen := 0
   332  	for name := range attrsS {
   333  		oldVal := ctyGetAttrMaybeNull(old, name)
   334  		newVal := ctyGetAttrMaybeNull(new, name)
   335  		if oldVal.IsNull() && newVal.IsNull() {
   336  			// Skip attributes where both old and new values are null
   337  			// (we do this early here so that we'll do our value alignment
   338  			// based on the longest attribute name that has a change, rather
   339  			// than the longest attribute name in the full set.)
   340  			continue
   341  		}
   342  
   343  		attrNames = append(attrNames, name)
   344  		displayAttrNames[name] = displayAttributeName(name)
   345  		if len(displayAttrNames[name]) > attrNameLen {
   346  			attrNameLen = len(displayAttrNames[name])
   347  		}
   348  	}
   349  	sort.Strings(attrNames)
   350  	if len(attrNames) == 0 {
   351  		return false
   352  	}
   353  
   354  	for _, name := range attrNames {
   355  		attrS := attrsS[name]
   356  		oldVal := ctyGetAttrMaybeNull(old, name)
   357  		newVal := ctyGetAttrMaybeNull(new, name)
   358  
   359  		result.bodyWritten = true
   360  		skipped := p.writeAttrDiff(displayAttrNames[name], attrS, oldVal, newVal, attrNameLen, indent, path)
   361  		if skipped {
   362  			result.skippedAttributes++
   363  		}
   364  	}
   365  
   366  	return true
   367  }
   368  
   369  // getPlanActionAndShow returns the action value
   370  // and a boolean for showJustNew. In this function we
   371  // modify the old and new values to remove any possible marks
   372  func getPlanActionAndShow(old cty.Value, new cty.Value) (plans.Action, bool) {
   373  	var action plans.Action
   374  	showJustNew := false
   375  	switch {
   376  	case old.IsNull():
   377  		action = plans.Create
   378  		showJustNew = true
   379  	case new.IsNull():
   380  		action = plans.Delete
   381  	case ctyEqualWithUnknown(old, new):
   382  		action = plans.NoOp
   383  		showJustNew = true
   384  	default:
   385  		action = plans.Update
   386  	}
   387  	return action, showJustNew
   388  }
   389  
   390  func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) bool {
   391  	path = append(path, cty.GetAttrStep{Name: name})
   392  	action, showJustNew := getPlanActionAndShow(old, new)
   393  
   394  	if action == plans.NoOp && !p.verbose && !identifyingAttribute(name, attrS) {
   395  		return true
   396  	}
   397  
   398  	if attrS.NestedType != nil {
   399  		p.writeNestedAttrDiff(name, attrS.NestedType, old, new, nameLen, indent, path, action, showJustNew)
   400  		return false
   401  	}
   402  
   403  	p.buf.WriteString("\n")
   404  
   405  	p.writeSensitivityWarning(old, new, indent, action, false)
   406  
   407  	p.buf.WriteString(strings.Repeat(" ", indent))
   408  	p.writeActionSymbol(action)
   409  
   410  	p.buf.WriteString(p.color.Color("[bold]"))
   411  	p.buf.WriteString(name)
   412  	p.buf.WriteString(p.color.Color("[reset]"))
   413  	p.buf.WriteString(strings.Repeat(" ", nameLen-len(name)))
   414  	p.buf.WriteString(" = ")
   415  
   416  	if attrS.Sensitive {
   417  		p.buf.WriteString("(sensitive value)")
   418  		if p.pathForcesNewResource(path) {
   419  			p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   420  		}
   421  	} else {
   422  		switch {
   423  		case showJustNew:
   424  			p.writeValue(new, action, indent+2)
   425  			if p.pathForcesNewResource(path) {
   426  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   427  			}
   428  		default:
   429  			// We show new even if it is null to emphasize the fact
   430  			// that it is being unset, since otherwise it is easy to
   431  			// misunderstand that the value is still set to the old value.
   432  			p.writeValueDiff(old, new, indent+2, path)
   433  		}
   434  	}
   435  
   436  	return false
   437  }
   438  
   439  // writeNestedAttrDiff is responsible for formatting Attributes with NestedTypes
   440  // in the diff.
   441  func (p *blockBodyDiffPrinter) writeNestedAttrDiff(
   442  	name string, objS *configschema.Object, old, new cty.Value,
   443  	nameLen, indent int, path cty.Path, action plans.Action, showJustNew bool) {
   444  
   445  	p.buf.WriteString("\n")
   446  	p.writeSensitivityWarning(old, new, indent, action, false)
   447  	p.buf.WriteString(strings.Repeat(" ", indent))
   448  	p.writeActionSymbol(action)
   449  
   450  	p.buf.WriteString(p.color.Color("[bold]"))
   451  	p.buf.WriteString(name)
   452  	p.buf.WriteString(p.color.Color("[reset]"))
   453  	p.buf.WriteString(strings.Repeat(" ", nameLen-len(name)))
   454  
   455  	if old.HasMark(marks.Sensitive) || new.HasMark(marks.Sensitive) {
   456  		p.buf.WriteString(" = (sensitive value)")
   457  		if p.pathForcesNewResource(path) {
   458  			p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   459  		}
   460  		return
   461  	}
   462  
   463  	result := &blockBodyDiffResult{}
   464  	switch objS.Nesting {
   465  	case configschema.NestingSingle:
   466  		p.buf.WriteString(" = {")
   467  		if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
   468  			p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   469  		}
   470  		p.writeAttrsDiff(objS.Attributes, old, new, indent+4, path, result)
   471  		p.writeSkippedAttr(result.skippedAttributes, indent+6)
   472  		p.buf.WriteString("\n")
   473  		p.buf.WriteString(strings.Repeat(" ", indent+2))
   474  		p.buf.WriteString("}")
   475  
   476  	case configschema.NestingList:
   477  		p.buf.WriteString(" = [")
   478  		if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
   479  			p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   480  		}
   481  		p.buf.WriteString("\n")
   482  
   483  		oldItems := ctyCollectionValues(old)
   484  		newItems := ctyCollectionValues(new)
   485  		// Here we intentionally preserve the index-based correspondance
   486  		// between old and new, rather than trying to detect insertions
   487  		// and removals in the list, because this more accurately reflects
   488  		// how Durgaform Core and providers will understand the change,
   489  		// particularly when the nested block contains computed attributes
   490  		// that will themselves maintain correspondance by index.
   491  
   492  		// commonLen is number of elements that exist in both lists, which
   493  		// will be presented as updates (~). Any additional items in one
   494  		// of the lists will be presented as either creates (+) or deletes (-)
   495  		// depending on which list they belong to. maxLen is the number of
   496  		// elements in that longer list.
   497  		var commonLen int
   498  		var maxLen int
   499  		// unchanged is the number of unchanged elements
   500  		var unchanged int
   501  
   502  		switch {
   503  		case len(oldItems) < len(newItems):
   504  			commonLen = len(oldItems)
   505  			maxLen = len(newItems)
   506  		default:
   507  			commonLen = len(newItems)
   508  			maxLen = len(oldItems)
   509  		}
   510  		for i := 0; i < maxLen; i++ {
   511  			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
   512  
   513  			var action plans.Action
   514  			var oldItem, newItem cty.Value
   515  			switch {
   516  			case i < commonLen:
   517  				oldItem = oldItems[i]
   518  				newItem = newItems[i]
   519  				if oldItem.RawEquals(newItem) {
   520  					action = plans.NoOp
   521  					unchanged++
   522  				} else {
   523  					action = plans.Update
   524  				}
   525  			case i < len(oldItems):
   526  				oldItem = oldItems[i]
   527  				newItem = cty.NullVal(oldItem.Type())
   528  				action = plans.Delete
   529  			case i < len(newItems):
   530  				newItem = newItems[i]
   531  				oldItem = cty.NullVal(newItem.Type())
   532  				action = plans.Create
   533  			default:
   534  				action = plans.NoOp
   535  			}
   536  
   537  			if action != plans.NoOp {
   538  				p.buf.WriteString(strings.Repeat(" ", indent+4))
   539  				p.writeActionSymbol(action)
   540  				p.buf.WriteString("{")
   541  
   542  				result := &blockBodyDiffResult{}
   543  				p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+8, path, result)
   544  				if action == plans.Update {
   545  					p.writeSkippedAttr(result.skippedAttributes, indent+10)
   546  				}
   547  				p.buf.WriteString("\n")
   548  
   549  				p.buf.WriteString(strings.Repeat(" ", indent+6))
   550  				p.buf.WriteString("},\n")
   551  			}
   552  		}
   553  		p.writeSkippedElems(unchanged, indent+6)
   554  		p.buf.WriteString(strings.Repeat(" ", indent+2))
   555  		p.buf.WriteString("]")
   556  
   557  		if !new.IsKnown() {
   558  			p.buf.WriteString(" -> (known after apply)")
   559  		}
   560  
   561  	case configschema.NestingSet:
   562  		oldItems := ctyCollectionValues(old)
   563  		newItems := ctyCollectionValues(new)
   564  
   565  		var all cty.Value
   566  		if len(oldItems)+len(newItems) > 0 {
   567  			allItems := make([]cty.Value, 0, len(oldItems)+len(newItems))
   568  			allItems = append(allItems, oldItems...)
   569  			allItems = append(allItems, newItems...)
   570  
   571  			all = cty.SetVal(allItems)
   572  		} else {
   573  			all = cty.SetValEmpty(old.Type().ElementType())
   574  		}
   575  
   576  		p.buf.WriteString(" = [")
   577  
   578  		var unchanged int
   579  
   580  		for it := all.ElementIterator(); it.Next(); {
   581  			_, val := it.Element()
   582  			var action plans.Action
   583  			var oldValue, newValue cty.Value
   584  			switch {
   585  			case !val.IsKnown():
   586  				action = plans.Update
   587  				newValue = val
   588  			case !new.IsKnown():
   589  				action = plans.Delete
   590  				// the value must have come from the old set
   591  				oldValue = val
   592  				// Mark the new val as null, but the entire set will be
   593  				// displayed as "(unknown after apply)"
   594  				newValue = cty.NullVal(val.Type())
   595  			case old.IsNull() || !old.HasElement(val).True():
   596  				action = plans.Create
   597  				oldValue = cty.NullVal(val.Type())
   598  				newValue = val
   599  			case new.IsNull() || !new.HasElement(val).True():
   600  				action = plans.Delete
   601  				oldValue = val
   602  				newValue = cty.NullVal(val.Type())
   603  			default:
   604  				action = plans.NoOp
   605  				oldValue = val
   606  				newValue = val
   607  			}
   608  
   609  			if action == plans.NoOp {
   610  				unchanged++
   611  				continue
   612  			}
   613  
   614  			p.buf.WriteString("\n")
   615  			p.buf.WriteString(strings.Repeat(" ", indent+4))
   616  			p.writeActionSymbol(action)
   617  			p.buf.WriteString("{")
   618  
   619  			if p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1]) {
   620  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   621  			}
   622  
   623  			path := append(path, cty.IndexStep{Key: val})
   624  			p.writeAttrsDiff(objS.Attributes, oldValue, newValue, indent+8, path, result)
   625  
   626  			p.buf.WriteString("\n")
   627  			p.buf.WriteString(strings.Repeat(" ", indent+6))
   628  			p.buf.WriteString("},")
   629  		}
   630  		p.buf.WriteString("\n")
   631  		p.writeSkippedElems(unchanged, indent+6)
   632  		p.buf.WriteString(strings.Repeat(" ", indent+2))
   633  		p.buf.WriteString("]")
   634  
   635  		if !new.IsKnown() {
   636  			p.buf.WriteString(" -> (known after apply)")
   637  		}
   638  
   639  	case configschema.NestingMap:
   640  		// For the sake of handling nested blocks, we'll treat a null map
   641  		// the same as an empty map since the config language doesn't
   642  		// distinguish these anyway.
   643  		old = ctyNullBlockMapAsEmpty(old)
   644  		new = ctyNullBlockMapAsEmpty(new)
   645  
   646  		oldItems := old.AsValueMap()
   647  
   648  		newItems := map[string]cty.Value{}
   649  
   650  		if new.IsKnown() {
   651  			newItems = new.AsValueMap()
   652  		}
   653  
   654  		allKeys := make(map[string]bool)
   655  		for k := range oldItems {
   656  			allKeys[k] = true
   657  		}
   658  		for k := range newItems {
   659  			allKeys[k] = true
   660  		}
   661  		allKeysOrder := make([]string, 0, len(allKeys))
   662  		for k := range allKeys {
   663  			allKeysOrder = append(allKeysOrder, k)
   664  		}
   665  		sort.Strings(allKeysOrder)
   666  
   667  		p.buf.WriteString(" = {\n")
   668  
   669  		// unchanged tracks the number of unchanged elements
   670  		unchanged := 0
   671  		for _, k := range allKeysOrder {
   672  			var action plans.Action
   673  			oldValue := oldItems[k]
   674  
   675  			newValue := newItems[k]
   676  			switch {
   677  			case oldValue == cty.NilVal:
   678  				oldValue = cty.NullVal(newValue.Type())
   679  				action = plans.Create
   680  			case newValue == cty.NilVal:
   681  				newValue = cty.NullVal(oldValue.Type())
   682  				action = plans.Delete
   683  			case !newValue.RawEquals(oldValue):
   684  				action = plans.Update
   685  			default:
   686  				action = plans.NoOp
   687  				unchanged++
   688  			}
   689  
   690  			if action != plans.NoOp {
   691  				p.buf.WriteString(strings.Repeat(" ", indent+4))
   692  				p.writeActionSymbol(action)
   693  				fmt.Fprintf(p.buf, "%q = {", k)
   694  				if p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1]) {
   695  					p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   696  				}
   697  
   698  				path := append(path, cty.IndexStep{Key: cty.StringVal(k)})
   699  				p.writeAttrsDiff(objS.Attributes, oldValue, newValue, indent+8, path, result)
   700  				p.writeSkippedAttr(result.skippedAttributes, indent+10)
   701  				p.buf.WriteString("\n")
   702  				p.buf.WriteString(strings.Repeat(" ", indent+6))
   703  				p.buf.WriteString("},\n")
   704  			}
   705  		}
   706  
   707  		p.writeSkippedElems(unchanged, indent+6)
   708  		p.buf.WriteString(strings.Repeat(" ", indent+2))
   709  		p.buf.WriteString("}")
   710  		if !new.IsKnown() {
   711  			p.buf.WriteString(" -> (known after apply)")
   712  		}
   713  	}
   714  }
   715  
   716  func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *configschema.NestedBlock, old, new cty.Value, blankBefore bool, indent int, path cty.Path) int {
   717  	skippedBlocks := 0
   718  	path = append(path, cty.GetAttrStep{Name: name})
   719  	if old.IsNull() && new.IsNull() {
   720  		// Nothing to do if both old and new is null
   721  		return skippedBlocks
   722  	}
   723  
   724  	// If either the old or the new value is marked,
   725  	// Display a special diff because it is irrelevant
   726  	// to list all obfuscated attributes as (sensitive)
   727  	if old.HasMark(marks.Sensitive) || new.HasMark(marks.Sensitive) {
   728  		p.writeSensitiveNestedBlockDiff(name, old, new, indent, blankBefore, path)
   729  		return 0
   730  	}
   731  
   732  	// Where old/new are collections representing a nesting mode other than
   733  	// NestingSingle, we assume the collection value can never be unknown
   734  	// since we always produce the container for the nested objects, even if
   735  	// the objects within are computed.
   736  
   737  	switch blockS.Nesting {
   738  	case configschema.NestingSingle, configschema.NestingGroup:
   739  		var action plans.Action
   740  		eqV := new.Equals(old)
   741  		switch {
   742  		case old.IsNull():
   743  			action = plans.Create
   744  		case new.IsNull():
   745  			action = plans.Delete
   746  		case !new.IsWhollyKnown() || !old.IsWhollyKnown():
   747  			// "old" should actually always be known due to our contract
   748  			// that old values must never be unknown, but we'll allow it
   749  			// anyway to be robust.
   750  			action = plans.Update
   751  		case !eqV.IsKnown() || !eqV.True():
   752  			action = plans.Update
   753  		}
   754  
   755  		skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, old, new, indent, blankBefore, path)
   756  		if skipped {
   757  			return 1
   758  		}
   759  	case configschema.NestingList:
   760  		// For the sake of handling nested blocks, we'll treat a null list
   761  		// the same as an empty list since the config language doesn't
   762  		// distinguish these anyway.
   763  		old = ctyNullBlockListAsEmpty(old)
   764  		new = ctyNullBlockListAsEmpty(new)
   765  
   766  		oldItems := ctyCollectionValues(old)
   767  		newItems := ctyCollectionValues(new)
   768  
   769  		// Here we intentionally preserve the index-based correspondance
   770  		// between old and new, rather than trying to detect insertions
   771  		// and removals in the list, because this more accurately reflects
   772  		// how Durgaform Core and providers will understand the change,
   773  		// particularly when the nested block contains computed attributes
   774  		// that will themselves maintain correspondance by index.
   775  
   776  		// commonLen is number of elements that exist in both lists, which
   777  		// will be presented as updates (~). Any additional items in one
   778  		// of the lists will be presented as either creates (+) or deletes (-)
   779  		// depending on which list they belong to.
   780  		var commonLen int
   781  		switch {
   782  		case len(oldItems) < len(newItems):
   783  			commonLen = len(oldItems)
   784  		default:
   785  			commonLen = len(newItems)
   786  		}
   787  
   788  		blankBeforeInner := blankBefore
   789  		for i := 0; i < commonLen; i++ {
   790  			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
   791  			oldItem := oldItems[i]
   792  			newItem := newItems[i]
   793  			action := plans.Update
   794  			if oldItem.RawEquals(newItem) {
   795  				action = plans.NoOp
   796  			}
   797  			skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldItem, newItem, indent, blankBeforeInner, path)
   798  			if skipped {
   799  				skippedBlocks++
   800  			} else {
   801  				blankBeforeInner = false
   802  			}
   803  		}
   804  		for i := commonLen; i < len(oldItems); i++ {
   805  			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
   806  			oldItem := oldItems[i]
   807  			newItem := cty.NullVal(oldItem.Type())
   808  			skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Delete, oldItem, newItem, indent, blankBeforeInner, path)
   809  			if skipped {
   810  				skippedBlocks++
   811  			} else {
   812  				blankBeforeInner = false
   813  			}
   814  		}
   815  		for i := commonLen; i < len(newItems); i++ {
   816  			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
   817  			newItem := newItems[i]
   818  			oldItem := cty.NullVal(newItem.Type())
   819  			skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Create, oldItem, newItem, indent, blankBeforeInner, path)
   820  			if skipped {
   821  				skippedBlocks++
   822  			} else {
   823  				blankBeforeInner = false
   824  			}
   825  		}
   826  	case configschema.NestingSet:
   827  		// For the sake of handling nested blocks, we'll treat a null set
   828  		// the same as an empty set since the config language doesn't
   829  		// distinguish these anyway.
   830  		old = ctyNullBlockSetAsEmpty(old)
   831  		new = ctyNullBlockSetAsEmpty(new)
   832  
   833  		oldItems := ctyCollectionValues(old)
   834  		newItems := ctyCollectionValues(new)
   835  
   836  		if (len(oldItems) + len(newItems)) == 0 {
   837  			// Nothing to do if both sets are empty
   838  			return 0
   839  		}
   840  
   841  		allItems := make([]cty.Value, 0, len(oldItems)+len(newItems))
   842  		allItems = append(allItems, oldItems...)
   843  		allItems = append(allItems, newItems...)
   844  		all := cty.SetVal(allItems)
   845  
   846  		blankBeforeInner := blankBefore
   847  		for it := all.ElementIterator(); it.Next(); {
   848  			_, val := it.Element()
   849  			var action plans.Action
   850  			var oldValue, newValue cty.Value
   851  			switch {
   852  			case !val.IsKnown():
   853  				action = plans.Update
   854  				newValue = val
   855  			case !old.HasElement(val).True():
   856  				action = plans.Create
   857  				oldValue = cty.NullVal(val.Type())
   858  				newValue = val
   859  			case !new.HasElement(val).True():
   860  				action = plans.Delete
   861  				oldValue = val
   862  				newValue = cty.NullVal(val.Type())
   863  			default:
   864  				action = plans.NoOp
   865  				oldValue = val
   866  				newValue = val
   867  			}
   868  			path := append(path, cty.IndexStep{Key: val})
   869  			skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldValue, newValue, indent, blankBeforeInner, path)
   870  			if skipped {
   871  				skippedBlocks++
   872  			} else {
   873  				blankBeforeInner = false
   874  			}
   875  		}
   876  
   877  	case configschema.NestingMap:
   878  		// For the sake of handling nested blocks, we'll treat a null map
   879  		// the same as an empty map since the config language doesn't
   880  		// distinguish these anyway.
   881  		old = ctyNullBlockMapAsEmpty(old)
   882  		new = ctyNullBlockMapAsEmpty(new)
   883  
   884  		oldItems := old.AsValueMap()
   885  		newItems := new.AsValueMap()
   886  		if (len(oldItems) + len(newItems)) == 0 {
   887  			// Nothing to do if both maps are empty
   888  			return 0
   889  		}
   890  
   891  		allKeys := make(map[string]bool)
   892  		for k := range oldItems {
   893  			allKeys[k] = true
   894  		}
   895  		for k := range newItems {
   896  			allKeys[k] = true
   897  		}
   898  		allKeysOrder := make([]string, 0, len(allKeys))
   899  		for k := range allKeys {
   900  			allKeysOrder = append(allKeysOrder, k)
   901  		}
   902  		sort.Strings(allKeysOrder)
   903  
   904  		blankBeforeInner := blankBefore
   905  		for _, k := range allKeysOrder {
   906  			var action plans.Action
   907  			oldValue := oldItems[k]
   908  			newValue := newItems[k]
   909  			switch {
   910  			case oldValue == cty.NilVal:
   911  				oldValue = cty.NullVal(newValue.Type())
   912  				action = plans.Create
   913  			case newValue == cty.NilVal:
   914  				newValue = cty.NullVal(oldValue.Type())
   915  				action = plans.Delete
   916  			case !newValue.RawEquals(oldValue):
   917  				action = plans.Update
   918  			default:
   919  				action = plans.NoOp
   920  			}
   921  
   922  			path := append(path, cty.IndexStep{Key: cty.StringVal(k)})
   923  			skipped := p.writeNestedBlockDiff(name, &k, &blockS.Block, action, oldValue, newValue, indent, blankBeforeInner, path)
   924  			if skipped {
   925  				skippedBlocks++
   926  			} else {
   927  				blankBeforeInner = false
   928  			}
   929  		}
   930  	}
   931  	return skippedBlocks
   932  }
   933  
   934  func (p *blockBodyDiffPrinter) writeSensitiveNestedBlockDiff(name string, old, new cty.Value, indent int, blankBefore bool, path cty.Path) {
   935  	var action plans.Action
   936  	switch {
   937  	case old.IsNull():
   938  		action = plans.Create
   939  	case new.IsNull():
   940  		action = plans.Delete
   941  	case !new.IsWhollyKnown() || !old.IsWhollyKnown():
   942  		// "old" should actually always be known due to our contract
   943  		// that old values must never be unknown, but we'll allow it
   944  		// anyway to be robust.
   945  		action = plans.Update
   946  	case !ctyEqualValueAndMarks(old, new):
   947  		action = plans.Update
   948  	}
   949  
   950  	if blankBefore {
   951  		p.buf.WriteRune('\n')
   952  	}
   953  
   954  	// New line before warning printing
   955  	p.buf.WriteRune('\n')
   956  	p.writeSensitivityWarning(old, new, indent, action, true)
   957  	p.buf.WriteString(strings.Repeat(" ", indent))
   958  	p.writeActionSymbol(action)
   959  	fmt.Fprintf(p.buf, "%s {", name)
   960  	if action != plans.NoOp && p.pathForcesNewResource(path) {
   961  		p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   962  	}
   963  	p.buf.WriteRune('\n')
   964  	p.buf.WriteString(strings.Repeat(" ", indent+4))
   965  	p.buf.WriteString("# At least one attribute in this block is (or was) sensitive,\n")
   966  	p.buf.WriteString(strings.Repeat(" ", indent+4))
   967  	p.buf.WriteString("# so its contents will not be displayed.")
   968  	p.buf.WriteRune('\n')
   969  	p.buf.WriteString(strings.Repeat(" ", indent+2))
   970  	p.buf.WriteString("}")
   971  }
   972  
   973  func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string, blockS *configschema.Block, action plans.Action, old, new cty.Value, indent int, blankBefore bool, path cty.Path) bool {
   974  	if action == plans.NoOp && !p.verbose {
   975  		return true
   976  	}
   977  
   978  	if blankBefore {
   979  		p.buf.WriteRune('\n')
   980  	}
   981  
   982  	p.buf.WriteString("\n")
   983  	p.buf.WriteString(strings.Repeat(" ", indent))
   984  	p.writeActionSymbol(action)
   985  
   986  	if label != nil {
   987  		fmt.Fprintf(p.buf, "%s %q {", name, *label)
   988  	} else {
   989  		fmt.Fprintf(p.buf, "%s {", name)
   990  	}
   991  
   992  	if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
   993  		p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   994  	}
   995  
   996  	result := p.writeBlockBodyDiff(blockS, old, new, indent+4, path)
   997  	if result.bodyWritten {
   998  		p.buf.WriteString("\n")
   999  		p.buf.WriteString(strings.Repeat(" ", indent+2))
  1000  	}
  1001  	p.buf.WriteString("}")
  1002  
  1003  	return false
  1004  }
  1005  
  1006  func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) {
  1007  	// Could check specifically for the sensitivity marker
  1008  	if val.HasMark(marks.Sensitive) {
  1009  		p.buf.WriteString("(sensitive)")
  1010  		return
  1011  	}
  1012  
  1013  	if !val.IsKnown() {
  1014  		p.buf.WriteString("(known after apply)")
  1015  		return
  1016  	}
  1017  	if val.IsNull() {
  1018  		p.buf.WriteString(p.color.Color("[dark_gray]null[reset]"))
  1019  		return
  1020  	}
  1021  
  1022  	ty := val.Type()
  1023  
  1024  	switch {
  1025  	case ty.IsPrimitiveType():
  1026  		switch ty {
  1027  		case cty.String:
  1028  			{
  1029  				// Special behavior for JSON strings containing array or object
  1030  				src := []byte(val.AsString())
  1031  				ty, err := ctyjson.ImpliedType(src)
  1032  				// check for the special case of "null", which decodes to nil,
  1033  				// and just allow it to be printed out directly
  1034  				if err == nil && !ty.IsPrimitiveType() && strings.TrimSpace(val.AsString()) != "null" {
  1035  					jv, err := ctyjson.Unmarshal(src, ty)
  1036  					if err == nil {
  1037  						p.buf.WriteString("jsonencode(")
  1038  						if jv.LengthInt() == 0 {
  1039  							p.writeValue(jv, action, 0)
  1040  						} else {
  1041  							p.buf.WriteByte('\n')
  1042  							p.buf.WriteString(strings.Repeat(" ", indent+4))
  1043  							p.writeValue(jv, action, indent+4)
  1044  							p.buf.WriteByte('\n')
  1045  							p.buf.WriteString(strings.Repeat(" ", indent))
  1046  						}
  1047  						p.buf.WriteByte(')')
  1048  						break // don't *also* do the normal behavior below
  1049  					}
  1050  				}
  1051  			}
  1052  
  1053  			if strings.Contains(val.AsString(), "\n") {
  1054  				// It's a multi-line string, so we want to use the multi-line
  1055  				// rendering so it'll be readable. Rather than re-implement
  1056  				// that here, we'll just re-use the multi-line string diff
  1057  				// printer with no changes, which ends up producing the
  1058  				// result we want here.
  1059  				// The path argument is nil because we don't track path
  1060  				// information into strings and we know that a string can't
  1061  				// have any indices or attributes that might need to be marked
  1062  				// as (requires replacement), which is what that argument is for.
  1063  				p.writeValueDiff(val, val, indent, nil)
  1064  				break
  1065  			}
  1066  
  1067  			fmt.Fprintf(p.buf, "%q", val.AsString())
  1068  		case cty.Bool:
  1069  			if val.True() {
  1070  				p.buf.WriteString("true")
  1071  			} else {
  1072  				p.buf.WriteString("false")
  1073  			}
  1074  		case cty.Number:
  1075  			bf := val.AsBigFloat()
  1076  			p.buf.WriteString(bf.Text('f', -1))
  1077  		default:
  1078  			// should never happen, since the above is exhaustive
  1079  			fmt.Fprintf(p.buf, "%#v", val)
  1080  		}
  1081  	case ty.IsListType() || ty.IsSetType() || ty.IsTupleType():
  1082  		p.buf.WriteString("[")
  1083  
  1084  		it := val.ElementIterator()
  1085  		for it.Next() {
  1086  			_, val := it.Element()
  1087  
  1088  			p.buf.WriteString("\n")
  1089  			p.buf.WriteString(strings.Repeat(" ", indent+2))
  1090  			p.writeActionSymbol(action)
  1091  			p.writeValue(val, action, indent+4)
  1092  			p.buf.WriteString(",")
  1093  		}
  1094  
  1095  		if val.LengthInt() > 0 {
  1096  			p.buf.WriteString("\n")
  1097  			p.buf.WriteString(strings.Repeat(" ", indent))
  1098  		}
  1099  		p.buf.WriteString("]")
  1100  	case ty.IsMapType():
  1101  		p.buf.WriteString("{")
  1102  
  1103  		keyLen := 0
  1104  		for it := val.ElementIterator(); it.Next(); {
  1105  			key, _ := it.Element()
  1106  			if keyStr := key.AsString(); len(keyStr) > keyLen {
  1107  				keyLen = len(keyStr)
  1108  			}
  1109  		}
  1110  
  1111  		for it := val.ElementIterator(); it.Next(); {
  1112  			key, val := it.Element()
  1113  
  1114  			p.buf.WriteString("\n")
  1115  			p.buf.WriteString(strings.Repeat(" ", indent+2))
  1116  			p.writeActionSymbol(action)
  1117  			p.writeValue(key, action, indent+4)
  1118  			p.buf.WriteString(strings.Repeat(" ", keyLen-len(key.AsString())))
  1119  			p.buf.WriteString(" = ")
  1120  			p.writeValue(val, action, indent+4)
  1121  		}
  1122  
  1123  		if val.LengthInt() > 0 {
  1124  			p.buf.WriteString("\n")
  1125  			p.buf.WriteString(strings.Repeat(" ", indent))
  1126  		}
  1127  		p.buf.WriteString("}")
  1128  	case ty.IsObjectType():
  1129  		p.buf.WriteString("{")
  1130  
  1131  		atys := ty.AttributeTypes()
  1132  		attrNames := make([]string, 0, len(atys))
  1133  		displayAttrNames := make(map[string]string, len(atys))
  1134  		nameLen := 0
  1135  		for attrName := range atys {
  1136  			attrNames = append(attrNames, attrName)
  1137  			displayAttrNames[attrName] = displayAttributeName(attrName)
  1138  			if len(displayAttrNames[attrName]) > nameLen {
  1139  				nameLen = len(displayAttrNames[attrName])
  1140  			}
  1141  		}
  1142  		sort.Strings(attrNames)
  1143  
  1144  		for _, attrName := range attrNames {
  1145  			val := val.GetAttr(attrName)
  1146  			displayAttrName := displayAttrNames[attrName]
  1147  
  1148  			p.buf.WriteString("\n")
  1149  			p.buf.WriteString(strings.Repeat(" ", indent+2))
  1150  			p.writeActionSymbol(action)
  1151  			p.buf.WriteString(displayAttrName)
  1152  			p.buf.WriteString(strings.Repeat(" ", nameLen-len(displayAttrName)))
  1153  			p.buf.WriteString(" = ")
  1154  			p.writeValue(val, action, indent+4)
  1155  		}
  1156  
  1157  		if len(attrNames) > 0 {
  1158  			p.buf.WriteString("\n")
  1159  			p.buf.WriteString(strings.Repeat(" ", indent))
  1160  		}
  1161  		p.buf.WriteString("}")
  1162  	}
  1163  }
  1164  
  1165  func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, path cty.Path) {
  1166  	ty := old.Type()
  1167  	typesEqual := ctyTypesEqual(ty, new.Type())
  1168  
  1169  	// We have some specialized diff implementations for certain complex
  1170  	// values where it's useful to see a visualization of the diff of
  1171  	// the nested elements rather than just showing the entire old and
  1172  	// new values verbatim.
  1173  	// However, these specialized implementations can apply only if both
  1174  	// values are known and non-null.
  1175  	if old.IsKnown() && new.IsKnown() && !old.IsNull() && !new.IsNull() && typesEqual {
  1176  		if old.HasMark(marks.Sensitive) || new.HasMark(marks.Sensitive) {
  1177  			p.buf.WriteString("(sensitive)")
  1178  			if p.pathForcesNewResource(path) {
  1179  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
  1180  			}
  1181  			return
  1182  		}
  1183  
  1184  		switch {
  1185  		case ty == cty.String:
  1186  			// We have special behavior for both multi-line strings in general
  1187  			// and for strings that can parse as JSON. For the JSON handling
  1188  			// to apply, both old and new must be valid JSON.
  1189  			// For single-line strings that don't parse as JSON we just fall
  1190  			// out of this switch block and do the default old -> new rendering.
  1191  			oldS := old.AsString()
  1192  			newS := new.AsString()
  1193  
  1194  			{
  1195  				// Special behavior for JSON strings containing object or
  1196  				// list values.
  1197  				oldBytes := []byte(oldS)
  1198  				newBytes := []byte(newS)
  1199  				oldType, oldErr := ctyjson.ImpliedType(oldBytes)
  1200  				newType, newErr := ctyjson.ImpliedType(newBytes)
  1201  				if oldErr == nil && newErr == nil && !(oldType.IsPrimitiveType() && newType.IsPrimitiveType()) {
  1202  					oldJV, oldErr := ctyjson.Unmarshal(oldBytes, oldType)
  1203  					newJV, newErr := ctyjson.Unmarshal(newBytes, newType)
  1204  					if oldErr == nil && newErr == nil {
  1205  						if !oldJV.RawEquals(newJV) { // two JSON values may differ only in insignificant whitespace
  1206  							p.buf.WriteString("jsonencode(")
  1207  							p.buf.WriteByte('\n')
  1208  							p.buf.WriteString(strings.Repeat(" ", indent+2))
  1209  							p.writeActionSymbol(plans.Update)
  1210  							p.writeValueDiff(oldJV, newJV, indent+4, path)
  1211  							p.buf.WriteByte('\n')
  1212  							p.buf.WriteString(strings.Repeat(" ", indent))
  1213  							p.buf.WriteByte(')')
  1214  						} else {
  1215  							// if they differ only in insignificant whitespace
  1216  							// then we'll note that but still expand out the
  1217  							// effective value.
  1218  							if p.pathForcesNewResource(path) {
  1219  								p.buf.WriteString(p.color.Color("jsonencode( [red]# whitespace changes force replacement[reset]"))
  1220  							} else {
  1221  								p.buf.WriteString(p.color.Color("jsonencode( [dim]# whitespace changes[reset]"))
  1222  							}
  1223  							p.buf.WriteByte('\n')
  1224  							p.buf.WriteString(strings.Repeat(" ", indent+4))
  1225  							p.writeValue(oldJV, plans.NoOp, indent+4)
  1226  							p.buf.WriteByte('\n')
  1227  							p.buf.WriteString(strings.Repeat(" ", indent))
  1228  							p.buf.WriteByte(')')
  1229  						}
  1230  						return
  1231  					}
  1232  				}
  1233  			}
  1234  
  1235  			if !strings.Contains(oldS, "\n") && !strings.Contains(newS, "\n") {
  1236  				break
  1237  			}
  1238  
  1239  			p.buf.WriteString("<<-EOT")
  1240  			if p.pathForcesNewResource(path) {
  1241  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
  1242  			}
  1243  			p.buf.WriteString("\n")
  1244  
  1245  			var oldLines, newLines []cty.Value
  1246  			{
  1247  				r := strings.NewReader(oldS)
  1248  				sc := bufio.NewScanner(r)
  1249  				for sc.Scan() {
  1250  					oldLines = append(oldLines, cty.StringVal(sc.Text()))
  1251  				}
  1252  			}
  1253  			{
  1254  				r := strings.NewReader(newS)
  1255  				sc := bufio.NewScanner(r)
  1256  				for sc.Scan() {
  1257  					newLines = append(newLines, cty.StringVal(sc.Text()))
  1258  				}
  1259  			}
  1260  
  1261  			// Optimization for strings which are exactly equal: just print
  1262  			// directly without calculating the sequence diff. This makes a
  1263  			// significant difference when this code path is reached via a
  1264  			// writeValue call with a large multi-line string.
  1265  			if oldS == newS {
  1266  				for _, line := range newLines {
  1267  					p.buf.WriteString(strings.Repeat(" ", indent+4))
  1268  					p.buf.WriteString(line.AsString())
  1269  					p.buf.WriteString("\n")
  1270  				}
  1271  			} else {
  1272  				diffLines := ctySequenceDiff(oldLines, newLines)
  1273  				for _, diffLine := range diffLines {
  1274  					p.buf.WriteString(strings.Repeat(" ", indent+2))
  1275  					p.writeActionSymbol(diffLine.Action)
  1276  
  1277  					switch diffLine.Action {
  1278  					case plans.NoOp, plans.Delete:
  1279  						p.buf.WriteString(diffLine.Before.AsString())
  1280  					case plans.Create:
  1281  						p.buf.WriteString(diffLine.After.AsString())
  1282  					default:
  1283  						// Should never happen since the above covers all
  1284  						// actions that ctySequenceDiff can return for strings
  1285  						p.buf.WriteString(diffLine.After.AsString())
  1286  
  1287  					}
  1288  					p.buf.WriteString("\n")
  1289  				}
  1290  			}
  1291  
  1292  			p.buf.WriteString(strings.Repeat(" ", indent)) // +4 here because there's no symbol
  1293  			p.buf.WriteString("EOT")
  1294  
  1295  			return
  1296  
  1297  		case ty.IsSetType():
  1298  			p.buf.WriteString("[")
  1299  			if p.pathForcesNewResource(path) {
  1300  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
  1301  			}
  1302  			p.buf.WriteString("\n")
  1303  
  1304  			var addedVals, removedVals, allVals []cty.Value
  1305  			for it := old.ElementIterator(); it.Next(); {
  1306  				_, val := it.Element()
  1307  				allVals = append(allVals, val)
  1308  				if new.HasElement(val).False() {
  1309  					removedVals = append(removedVals, val)
  1310  				}
  1311  			}
  1312  			for it := new.ElementIterator(); it.Next(); {
  1313  				_, val := it.Element()
  1314  				allVals = append(allVals, val)
  1315  				if val.IsKnown() && old.HasElement(val).False() {
  1316  					addedVals = append(addedVals, val)
  1317  				}
  1318  			}
  1319  
  1320  			var all, added, removed cty.Value
  1321  			if len(allVals) > 0 {
  1322  				all = cty.SetVal(allVals)
  1323  			} else {
  1324  				all = cty.SetValEmpty(ty.ElementType())
  1325  			}
  1326  			if len(addedVals) > 0 {
  1327  				added = cty.SetVal(addedVals)
  1328  			} else {
  1329  				added = cty.SetValEmpty(ty.ElementType())
  1330  			}
  1331  			if len(removedVals) > 0 {
  1332  				removed = cty.SetVal(removedVals)
  1333  			} else {
  1334  				removed = cty.SetValEmpty(ty.ElementType())
  1335  			}
  1336  
  1337  			suppressedElements := 0
  1338  			for it := all.ElementIterator(); it.Next(); {
  1339  				_, val := it.Element()
  1340  
  1341  				var action plans.Action
  1342  				switch {
  1343  				case !val.IsKnown():
  1344  					action = plans.Update
  1345  				case added.HasElement(val).True():
  1346  					action = plans.Create
  1347  				case removed.HasElement(val).True():
  1348  					action = plans.Delete
  1349  				default:
  1350  					action = plans.NoOp
  1351  				}
  1352  
  1353  				if action == plans.NoOp && !p.verbose {
  1354  					suppressedElements++
  1355  					continue
  1356  				}
  1357  
  1358  				p.buf.WriteString(strings.Repeat(" ", indent+2))
  1359  				p.writeActionSymbol(action)
  1360  				p.writeValue(val, action, indent+4)
  1361  				p.buf.WriteString(",\n")
  1362  			}
  1363  
  1364  			if suppressedElements > 0 {
  1365  				p.writeActionSymbol(plans.NoOp)
  1366  				p.buf.WriteString(strings.Repeat(" ", indent+2))
  1367  				noun := "elements"
  1368  				if suppressedElements == 1 {
  1369  					noun = "element"
  1370  				}
  1371  				p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun))
  1372  				p.buf.WriteString("\n")
  1373  			}
  1374  
  1375  			p.buf.WriteString(strings.Repeat(" ", indent))
  1376  			p.buf.WriteString("]")
  1377  			return
  1378  		case ty.IsListType() || ty.IsTupleType():
  1379  			p.buf.WriteString("[")
  1380  			if p.pathForcesNewResource(path) {
  1381  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
  1382  			}
  1383  			p.buf.WriteString("\n")
  1384  
  1385  			elemDiffs := ctySequenceDiff(old.AsValueSlice(), new.AsValueSlice())
  1386  
  1387  			// Maintain a stack of suppressed lines in the diff for later
  1388  			// display or elision
  1389  			var suppressedElements []*plans.Change
  1390  			var changeShown bool
  1391  
  1392  			for i := 0; i < len(elemDiffs); i++ {
  1393  				if !p.verbose {
  1394  					for i < len(elemDiffs) && elemDiffs[i].Action == plans.NoOp {
  1395  						suppressedElements = append(suppressedElements, elemDiffs[i])
  1396  						i++
  1397  					}
  1398  				}
  1399  
  1400  				// If we have some suppressed elements on the stack…
  1401  				if len(suppressedElements) > 0 {
  1402  					// If we've just rendered a change, display the first
  1403  					// element in the stack as context
  1404  					if changeShown {
  1405  						elemDiff := suppressedElements[0]
  1406  						p.buf.WriteString(strings.Repeat(" ", indent+4))
  1407  						p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
  1408  						p.buf.WriteString(",\n")
  1409  						suppressedElements = suppressedElements[1:]
  1410  					}
  1411  
  1412  					hidden := len(suppressedElements)
  1413  
  1414  					// If we're not yet at the end of the list, capture the
  1415  					// last element on the stack as context for the upcoming
  1416  					// change to be rendered
  1417  					var nextContextDiff *plans.Change
  1418  					if hidden > 0 && i < len(elemDiffs) {
  1419  						hidden--
  1420  						nextContextDiff = suppressedElements[hidden]
  1421  					}
  1422  
  1423  					// If there are still hidden elements, show an elision
  1424  					// statement counting them
  1425  					if hidden > 0 {
  1426  						p.writeActionSymbol(plans.NoOp)
  1427  						p.buf.WriteString(strings.Repeat(" ", indent+2))
  1428  						noun := "elements"
  1429  						if hidden == 1 {
  1430  							noun = "element"
  1431  						}
  1432  						p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), hidden, noun))
  1433  						p.buf.WriteString("\n")
  1434  					}
  1435  
  1436  					// Display the next context diff if it was captured above
  1437  					if nextContextDiff != nil {
  1438  						p.buf.WriteString(strings.Repeat(" ", indent+4))
  1439  						p.writeValue(nextContextDiff.After, nextContextDiff.Action, indent+4)
  1440  						p.buf.WriteString(",\n")
  1441  					}
  1442  
  1443  					// Suppressed elements have now been handled so clear them again
  1444  					suppressedElements = nil
  1445  				}
  1446  
  1447  				if i >= len(elemDiffs) {
  1448  					break
  1449  				}
  1450  
  1451  				elemDiff := elemDiffs[i]
  1452  				p.buf.WriteString(strings.Repeat(" ", indent+2))
  1453  				p.writeActionSymbol(elemDiff.Action)
  1454  				switch elemDiff.Action {
  1455  				case plans.NoOp, plans.Delete:
  1456  					p.writeValue(elemDiff.Before, elemDiff.Action, indent+4)
  1457  				case plans.Update:
  1458  					p.writeValueDiff(elemDiff.Before, elemDiff.After, indent+4, path)
  1459  				case plans.Create:
  1460  					p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
  1461  				default:
  1462  					// Should never happen since the above covers all
  1463  					// actions that ctySequenceDiff can return.
  1464  					p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
  1465  				}
  1466  
  1467  				p.buf.WriteString(",\n")
  1468  				changeShown = true
  1469  			}
  1470  
  1471  			p.buf.WriteString(strings.Repeat(" ", indent))
  1472  			p.buf.WriteString("]")
  1473  
  1474  			return
  1475  
  1476  		case ty.IsMapType():
  1477  			p.buf.WriteString("{")
  1478  			if p.pathForcesNewResource(path) {
  1479  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
  1480  			}
  1481  			p.buf.WriteString("\n")
  1482  
  1483  			var allKeys []string
  1484  			keyLen := 0
  1485  			for it := old.ElementIterator(); it.Next(); {
  1486  				k, _ := it.Element()
  1487  				keyStr := k.AsString()
  1488  				allKeys = append(allKeys, keyStr)
  1489  				if len(keyStr) > keyLen {
  1490  					keyLen = len(keyStr)
  1491  				}
  1492  			}
  1493  			for it := new.ElementIterator(); it.Next(); {
  1494  				k, _ := it.Element()
  1495  				keyStr := k.AsString()
  1496  				allKeys = append(allKeys, keyStr)
  1497  				if len(keyStr) > keyLen {
  1498  					keyLen = len(keyStr)
  1499  				}
  1500  			}
  1501  
  1502  			sort.Strings(allKeys)
  1503  
  1504  			suppressedElements := 0
  1505  			lastK := ""
  1506  			for i, k := range allKeys {
  1507  				if i > 0 && lastK == k {
  1508  					continue // skip duplicates (list is sorted)
  1509  				}
  1510  				lastK = k
  1511  
  1512  				kV := cty.StringVal(k)
  1513  				var action plans.Action
  1514  				if old.HasIndex(kV).False() {
  1515  					action = plans.Create
  1516  				} else if new.HasIndex(kV).False() {
  1517  					action = plans.Delete
  1518  				}
  1519  
  1520  				if old.HasIndex(kV).True() && new.HasIndex(kV).True() {
  1521  					if ctyEqualValueAndMarks(old.Index(kV), new.Index(kV)) {
  1522  						action = plans.NoOp
  1523  					} else {
  1524  						action = plans.Update
  1525  					}
  1526  				}
  1527  
  1528  				if action == plans.NoOp && !p.verbose {
  1529  					suppressedElements++
  1530  					continue
  1531  				}
  1532  
  1533  				path := append(path, cty.IndexStep{Key: kV})
  1534  
  1535  				oldV := old.Index(kV)
  1536  				newV := new.Index(kV)
  1537  				p.writeSensitivityWarning(oldV, newV, indent+2, action, false)
  1538  
  1539  				p.buf.WriteString(strings.Repeat(" ", indent+2))
  1540  				p.writeActionSymbol(action)
  1541  				p.writeValue(cty.StringVal(k), action, indent+4)
  1542  				p.buf.WriteString(strings.Repeat(" ", keyLen-len(k)))
  1543  				p.buf.WriteString(" = ")
  1544  				switch action {
  1545  				case plans.Create, plans.NoOp:
  1546  					v := new.Index(kV)
  1547  					if v.HasMark(marks.Sensitive) {
  1548  						p.buf.WriteString("(sensitive)")
  1549  					} else {
  1550  						p.writeValue(v, action, indent+4)
  1551  					}
  1552  				case plans.Delete:
  1553  					oldV := old.Index(kV)
  1554  					newV := cty.NullVal(oldV.Type())
  1555  					p.writeValueDiff(oldV, newV, indent+4, path)
  1556  				default:
  1557  					if oldV.HasMark(marks.Sensitive) || newV.HasMark(marks.Sensitive) {
  1558  						p.buf.WriteString("(sensitive)")
  1559  					} else {
  1560  						p.writeValueDiff(oldV, newV, indent+4, path)
  1561  					}
  1562  				}
  1563  
  1564  				p.buf.WriteByte('\n')
  1565  			}
  1566  
  1567  			if suppressedElements > 0 {
  1568  				p.writeActionSymbol(plans.NoOp)
  1569  				p.buf.WriteString(strings.Repeat(" ", indent+2))
  1570  				noun := "elements"
  1571  				if suppressedElements == 1 {
  1572  					noun = "element"
  1573  				}
  1574  				p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun))
  1575  				p.buf.WriteString("\n")
  1576  			}
  1577  
  1578  			p.buf.WriteString(strings.Repeat(" ", indent))
  1579  			p.buf.WriteString("}")
  1580  
  1581  			return
  1582  		case ty.IsObjectType():
  1583  			p.buf.WriteString("{")
  1584  			p.buf.WriteString("\n")
  1585  
  1586  			forcesNewResource := p.pathForcesNewResource(path)
  1587  
  1588  			var allKeys []string
  1589  			displayKeys := make(map[string]string)
  1590  			keyLen := 0
  1591  			for it := old.ElementIterator(); it.Next(); {
  1592  				k, _ := it.Element()
  1593  				keyStr := k.AsString()
  1594  				allKeys = append(allKeys, keyStr)
  1595  				displayKeys[keyStr] = displayAttributeName(keyStr)
  1596  				if len(displayKeys[keyStr]) > keyLen {
  1597  					keyLen = len(displayKeys[keyStr])
  1598  				}
  1599  			}
  1600  			for it := new.ElementIterator(); it.Next(); {
  1601  				k, _ := it.Element()
  1602  				keyStr := k.AsString()
  1603  				allKeys = append(allKeys, keyStr)
  1604  				displayKeys[keyStr] = displayAttributeName(keyStr)
  1605  				if len(displayKeys[keyStr]) > keyLen {
  1606  					keyLen = len(displayKeys[keyStr])
  1607  				}
  1608  			}
  1609  
  1610  			sort.Strings(allKeys)
  1611  
  1612  			suppressedElements := 0
  1613  			lastK := ""
  1614  			for i, k := range allKeys {
  1615  				if i > 0 && lastK == k {
  1616  					continue // skip duplicates (list is sorted)
  1617  				}
  1618  				lastK = k
  1619  
  1620  				kV := k
  1621  				var action plans.Action
  1622  				if !old.Type().HasAttribute(kV) {
  1623  					action = plans.Create
  1624  				} else if !new.Type().HasAttribute(kV) {
  1625  					action = plans.Delete
  1626  				} else if ctyEqualValueAndMarks(old.GetAttr(kV), new.GetAttr(kV)) {
  1627  					action = plans.NoOp
  1628  				} else {
  1629  					action = plans.Update
  1630  				}
  1631  
  1632  				// TODO: If in future we have a schema associated with this
  1633  				// object, we should pass the attribute's schema to
  1634  				// identifyingAttribute here.
  1635  				if action == plans.NoOp && !p.verbose && !identifyingAttribute(k, nil) {
  1636  					suppressedElements++
  1637  					continue
  1638  				}
  1639  
  1640  				path := append(path, cty.GetAttrStep{Name: kV})
  1641  
  1642  				p.buf.WriteString(strings.Repeat(" ", indent+2))
  1643  				p.writeActionSymbol(action)
  1644  				p.buf.WriteString(displayKeys[k])
  1645  				p.buf.WriteString(strings.Repeat(" ", keyLen-len(displayKeys[k])))
  1646  				p.buf.WriteString(" = ")
  1647  
  1648  				switch action {
  1649  				case plans.Create, plans.NoOp:
  1650  					v := new.GetAttr(kV)
  1651  					p.writeValue(v, action, indent+4)
  1652  				case plans.Delete:
  1653  					oldV := old.GetAttr(kV)
  1654  					newV := cty.NullVal(oldV.Type())
  1655  					p.writeValueDiff(oldV, newV, indent+4, path)
  1656  				default:
  1657  					oldV := old.GetAttr(kV)
  1658  					newV := new.GetAttr(kV)
  1659  					p.writeValueDiff(oldV, newV, indent+4, path)
  1660  				}
  1661  
  1662  				p.buf.WriteString("\n")
  1663  			}
  1664  
  1665  			if suppressedElements > 0 {
  1666  				p.writeActionSymbol(plans.NoOp)
  1667  				p.buf.WriteString(strings.Repeat(" ", indent+2))
  1668  				noun := "elements"
  1669  				if suppressedElements == 1 {
  1670  					noun = "element"
  1671  				}
  1672  				p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun))
  1673  				p.buf.WriteString("\n")
  1674  			}
  1675  
  1676  			p.buf.WriteString(strings.Repeat(" ", indent))
  1677  			p.buf.WriteString("}")
  1678  
  1679  			if forcesNewResource {
  1680  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
  1681  			}
  1682  			return
  1683  		}
  1684  	}
  1685  
  1686  	// In all other cases, we just show the new and old values as-is
  1687  	p.writeValue(old, plans.Delete, indent)
  1688  	if new.IsNull() {
  1689  		p.buf.WriteString(p.color.Color(" [dark_gray]->[reset] "))
  1690  	} else {
  1691  		p.buf.WriteString(p.color.Color(" [yellow]->[reset] "))
  1692  	}
  1693  
  1694  	p.writeValue(new, plans.Create, indent)
  1695  	if p.pathForcesNewResource(path) {
  1696  		p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
  1697  	}
  1698  }
  1699  
  1700  // writeActionSymbol writes a symbol to represent the given action, followed
  1701  // by a space.
  1702  //
  1703  // It only supports the actions that can be represented with a single character:
  1704  // Create, Delete, Update and NoAction.
  1705  func (p *blockBodyDiffPrinter) writeActionSymbol(action plans.Action) {
  1706  	switch action {
  1707  	case plans.Create:
  1708  		p.buf.WriteString(p.color.Color("[green]+[reset] "))
  1709  	case plans.Delete:
  1710  		p.buf.WriteString(p.color.Color("[red]-[reset] "))
  1711  	case plans.Update:
  1712  		p.buf.WriteString(p.color.Color("[yellow]~[reset] "))
  1713  	case plans.NoOp:
  1714  		p.buf.WriteString("  ")
  1715  	default:
  1716  		// Should never happen
  1717  		p.buf.WriteString(p.color.Color("? "))
  1718  	}
  1719  }
  1720  
  1721  func (p *blockBodyDiffPrinter) writeSensitivityWarning(old, new cty.Value, indent int, action plans.Action, isBlock bool) {
  1722  	// Dont' show this warning for create or delete
  1723  	if action == plans.Create || action == plans.Delete {
  1724  		return
  1725  	}
  1726  
  1727  	// Customize the warning based on if it is an attribute or block
  1728  	diffType := "attribute value"
  1729  	if isBlock {
  1730  		diffType = "block"
  1731  	}
  1732  
  1733  	// If only attribute sensitivity is changing, clarify that the value is unchanged
  1734  	var valueUnchangedSuffix string
  1735  	if !isBlock {
  1736  		oldUnmarked, _ := old.UnmarkDeep()
  1737  		newUnmarked, _ := new.UnmarkDeep()
  1738  		if oldUnmarked.RawEquals(newUnmarked) {
  1739  			valueUnchangedSuffix = " The value is unchanged."
  1740  		}
  1741  	}
  1742  
  1743  	if new.HasMark(marks.Sensitive) && !old.HasMark(marks.Sensitive) {
  1744  		p.buf.WriteString(strings.Repeat(" ", indent))
  1745  		p.buf.WriteString(fmt.Sprintf(p.color.Color("# [yellow]Warning:[reset] this %s will be marked as sensitive and will not\n"), diffType))
  1746  		p.buf.WriteString(strings.Repeat(" ", indent))
  1747  		p.buf.WriteString(fmt.Sprintf("# display in UI output after applying this change.%s\n", valueUnchangedSuffix))
  1748  	}
  1749  
  1750  	// Note if changing this attribute will change its sensitivity
  1751  	if old.HasMark(marks.Sensitive) && !new.HasMark(marks.Sensitive) {
  1752  		p.buf.WriteString(strings.Repeat(" ", indent))
  1753  		p.buf.WriteString(fmt.Sprintf(p.color.Color("# [yellow]Warning:[reset] this %s will no longer be marked as sensitive\n"), diffType))
  1754  		p.buf.WriteString(strings.Repeat(" ", indent))
  1755  		p.buf.WriteString(fmt.Sprintf("# after applying this change.%s\n", valueUnchangedSuffix))
  1756  	}
  1757  }
  1758  
  1759  func (p *blockBodyDiffPrinter) pathForcesNewResource(path cty.Path) bool {
  1760  	if !p.action.IsReplace() || p.requiredReplace.Empty() {
  1761  		// "requiredReplace" only applies when the instance is being replaced,
  1762  		// and we should only inspect that set if it is not empty
  1763  		return false
  1764  	}
  1765  	return p.requiredReplace.Has(path)
  1766  }
  1767  
  1768  func ctyEmptyString(value cty.Value) bool {
  1769  	if !value.IsNull() && value.IsKnown() {
  1770  		valueType := value.Type()
  1771  		if valueType == cty.String && value.AsString() == "" {
  1772  			return true
  1773  		}
  1774  	}
  1775  	return false
  1776  }
  1777  
  1778  func ctyGetAttrMaybeNull(val cty.Value, name string) cty.Value {
  1779  	attrType := val.Type().AttributeType(name)
  1780  
  1781  	if val.IsNull() {
  1782  		return cty.NullVal(attrType)
  1783  	}
  1784  
  1785  	// We treat "" as null here
  1786  	// as existing SDK doesn't support null yet.
  1787  	// This allows us to avoid spurious diffs
  1788  	// until we introduce null to the SDK.
  1789  	attrValue := val.GetAttr(name)
  1790  	// If the value is marked, the ctyEmptyString function will fail
  1791  	if !val.ContainsMarked() && ctyEmptyString(attrValue) {
  1792  		return cty.NullVal(attrType)
  1793  	}
  1794  
  1795  	return attrValue
  1796  }
  1797  
  1798  func ctyCollectionValues(val cty.Value) []cty.Value {
  1799  	if !val.IsKnown() || val.IsNull() {
  1800  		return nil
  1801  	}
  1802  
  1803  	ret := make([]cty.Value, 0, val.LengthInt())
  1804  	for it := val.ElementIterator(); it.Next(); {
  1805  		_, value := it.Element()
  1806  		ret = append(ret, value)
  1807  	}
  1808  	return ret
  1809  }
  1810  
  1811  // ctySequenceDiff returns differences between given sequences of cty.Value(s)
  1812  // in the form of Create, Delete, or Update actions (for objects).
  1813  func ctySequenceDiff(old, new []cty.Value) []*plans.Change {
  1814  	var ret []*plans.Change
  1815  	lcs := objchange.LongestCommonSubsequence(old, new)
  1816  	var oldI, newI, lcsI int
  1817  	for oldI < len(old) || newI < len(new) || lcsI < len(lcs) {
  1818  		// We first process items in the old and new sequences which are not
  1819  		// equal to the current common sequence item.  Old items are marked as
  1820  		// deletions, and new items are marked as additions.
  1821  		//
  1822  		// There is an exception for deleted & created object items, which we
  1823  		// try to render as updates where that makes sense.
  1824  		for oldI < len(old) && (lcsI >= len(lcs) || !old[oldI].RawEquals(lcs[lcsI])) {
  1825  			// Render this as an object update if all of these are true:
  1826  			//
  1827  			// - the current old item is an object;
  1828  			// - there's a current new item which is also an object;
  1829  			// - either there are no common items left, or the current new item
  1830  			//   doesn't equal the current common item.
  1831  			//
  1832  			// Why do we need the the last clause? If we have current items in all
  1833  			// three sequences, and the current new item is equal to a common item,
  1834  			// then we should just need to advance the old item list and we'll
  1835  			// eventually find a common item matching both old and new.
  1836  			//
  1837  			// This combination of conditions allows us to render an object update
  1838  			// diff instead of a combination of delete old & create new.
  1839  			isObjectDiff := old[oldI].Type().IsObjectType() && newI < len(new) && new[newI].Type().IsObjectType() && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI]))
  1840  			if isObjectDiff {
  1841  				ret = append(ret, &plans.Change{
  1842  					Action: plans.Update,
  1843  					Before: old[oldI],
  1844  					After:  new[newI],
  1845  				})
  1846  				oldI++
  1847  				newI++ // we also consume the next "new" in this case
  1848  				continue
  1849  			}
  1850  
  1851  			// Otherwise, this item is not part of the common sequence, so
  1852  			// render as a deletion.
  1853  			ret = append(ret, &plans.Change{
  1854  				Action: plans.Delete,
  1855  				Before: old[oldI],
  1856  				After:  cty.NullVal(old[oldI].Type()),
  1857  			})
  1858  			oldI++
  1859  		}
  1860  		for newI < len(new) && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI])) {
  1861  			ret = append(ret, &plans.Change{
  1862  				Action: plans.Create,
  1863  				Before: cty.NullVal(new[newI].Type()),
  1864  				After:  new[newI],
  1865  			})
  1866  			newI++
  1867  		}
  1868  
  1869  		// When we've exhausted the old & new sequences of items which are not
  1870  		// in the common subsequence, we render a common item and continue.
  1871  		if lcsI < len(lcs) {
  1872  			ret = append(ret, &plans.Change{
  1873  				Action: plans.NoOp,
  1874  				Before: lcs[lcsI],
  1875  				After:  lcs[lcsI],
  1876  			})
  1877  
  1878  			// All of our indexes advance together now, since the line
  1879  			// is common to all three sequences.
  1880  			lcsI++
  1881  			oldI++
  1882  			newI++
  1883  		}
  1884  	}
  1885  	return ret
  1886  }
  1887  
  1888  // ctyEqualValueAndMarks checks equality of two possibly-marked values,
  1889  // considering partially-unknown values and equal values with different marks
  1890  // as inequal
  1891  func ctyEqualWithUnknown(old, new cty.Value) bool {
  1892  	if !old.IsWhollyKnown() || !new.IsWhollyKnown() {
  1893  		return false
  1894  	}
  1895  	return ctyEqualValueAndMarks(old, new)
  1896  }
  1897  
  1898  // ctyEqualValueAndMarks checks equality of two possibly-marked values,
  1899  // considering equal values with different marks as inequal
  1900  func ctyEqualValueAndMarks(old, new cty.Value) bool {
  1901  	oldUnmarked, oldMarks := old.UnmarkDeep()
  1902  	newUnmarked, newMarks := new.UnmarkDeep()
  1903  	sameValue := oldUnmarked.Equals(newUnmarked)
  1904  	return sameValue.IsKnown() && sameValue.True() && oldMarks.Equal(newMarks)
  1905  }
  1906  
  1907  // ctyTypesEqual checks equality of two types more loosely
  1908  // by avoiding checks of object/tuple elements
  1909  // as we render differences on element-by-element basis anyway
  1910  func ctyTypesEqual(oldT, newT cty.Type) bool {
  1911  	if oldT.IsObjectType() && newT.IsObjectType() {
  1912  		return true
  1913  	}
  1914  	if oldT.IsTupleType() && newT.IsTupleType() {
  1915  		return true
  1916  	}
  1917  	return oldT.Equals(newT)
  1918  }
  1919  
  1920  func ctyEnsurePathCapacity(path cty.Path, minExtra int) cty.Path {
  1921  	if cap(path)-len(path) >= minExtra {
  1922  		return path
  1923  	}
  1924  	newCap := cap(path) * 2
  1925  	if newCap < (len(path) + minExtra) {
  1926  		newCap = len(path) + minExtra
  1927  	}
  1928  	newPath := make(cty.Path, len(path), newCap)
  1929  	copy(newPath, path)
  1930  	return newPath
  1931  }
  1932  
  1933  // ctyNullBlockListAsEmpty either returns the given value verbatim if it is non-nil
  1934  // or returns an empty value of a suitable type to serve as a placeholder for it.
  1935  //
  1936  // In particular, this function handles the special situation where a "list" is
  1937  // actually represented as a tuple type where nested blocks contain
  1938  // dynamically-typed values.
  1939  func ctyNullBlockListAsEmpty(in cty.Value) cty.Value {
  1940  	if !in.IsNull() {
  1941  		return in
  1942  	}
  1943  	if ty := in.Type(); ty.IsListType() {
  1944  		return cty.ListValEmpty(ty.ElementType())
  1945  	}
  1946  	return cty.EmptyTupleVal // must need a tuple, then
  1947  }
  1948  
  1949  // ctyNullBlockMapAsEmpty either returns the given value verbatim if it is non-nil
  1950  // or returns an empty value of a suitable type to serve as a placeholder for it.
  1951  //
  1952  // In particular, this function handles the special situation where a "map" is
  1953  // actually represented as an object type where nested blocks contain
  1954  // dynamically-typed values.
  1955  func ctyNullBlockMapAsEmpty(in cty.Value) cty.Value {
  1956  	if !in.IsNull() {
  1957  		return in
  1958  	}
  1959  	if ty := in.Type(); ty.IsMapType() {
  1960  		return cty.MapValEmpty(ty.ElementType())
  1961  	}
  1962  	return cty.EmptyObjectVal // must need an object, then
  1963  }
  1964  
  1965  // ctyNullBlockSetAsEmpty either returns the given value verbatim if it is non-nil
  1966  // or returns an empty value of a suitable type to serve as a placeholder for it.
  1967  func ctyNullBlockSetAsEmpty(in cty.Value) cty.Value {
  1968  	if !in.IsNull() {
  1969  		return in
  1970  	}
  1971  	// Dynamically-typed attributes are not supported inside blocks backed by
  1972  	// sets, so our result here is always a set.
  1973  	return cty.SetValEmpty(in.Type().ElementType())
  1974  }
  1975  
  1976  // DiffActionSymbol returns a string that, once passed through a
  1977  // colorstring.Colorize, will produce a result that can be written
  1978  // to a terminal to produce a symbol made of three printable
  1979  // characters, possibly interspersed with VT100 color codes.
  1980  func DiffActionSymbol(action plans.Action) string {
  1981  	switch action {
  1982  	case plans.DeleteThenCreate:
  1983  		return "[red]-[reset]/[green]+[reset]"
  1984  	case plans.CreateThenDelete:
  1985  		return "[green]+[reset]/[red]-[reset]"
  1986  	case plans.Create:
  1987  		return "  [green]+[reset]"
  1988  	case plans.Delete:
  1989  		return "  [red]-[reset]"
  1990  	case plans.Read:
  1991  		return " [cyan]<=[reset]"
  1992  	case plans.Update:
  1993  		return "  [yellow]~[reset]"
  1994  	default:
  1995  		return "  ?"
  1996  	}
  1997  }
  1998  
  1999  // Extremely coarse heuristic for determining whether or not a given attribute
  2000  // name is important for identifying a resource. In the future, this may be
  2001  // replaced by a flag in the schema, but for now this is likely to be good
  2002  // enough.
  2003  func identifyingAttribute(name string, attrSchema *configschema.Attribute) bool {
  2004  	return name == "id" || name == "tags" || name == "name"
  2005  }
  2006  
  2007  func (p *blockBodyDiffPrinter) writeSkippedAttr(skipped, indent int) {
  2008  	if skipped > 0 {
  2009  		noun := "attributes"
  2010  		if skipped == 1 {
  2011  			noun = "attribute"
  2012  		}
  2013  		p.buf.WriteString("\n")
  2014  		p.buf.WriteString(strings.Repeat(" ", indent))
  2015  		p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), skipped, noun))
  2016  	}
  2017  }
  2018  
  2019  func (p *blockBodyDiffPrinter) writeSkippedElems(skipped, indent int) {
  2020  	if skipped > 0 {
  2021  		noun := "elements"
  2022  		if skipped == 1 {
  2023  			noun = "element"
  2024  		}
  2025  		p.buf.WriteString(strings.Repeat(" ", indent))
  2026  		p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), skipped, noun))
  2027  		p.buf.WriteString("\n")
  2028  	}
  2029  }
  2030  
  2031  func displayAttributeName(name string) string {
  2032  	if !hclsyntax.ValidIdentifier(name) {
  2033  		return fmt.Sprintf("%q", name)
  2034  	}
  2035  	return name
  2036  }