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