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