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