github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/display/object_diff.go (about)

     1  // Copyright 2016-2018, Pulumi Corporation.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package display
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"reflect"
    23  	"sort"
    24  	"strconv"
    25  	"strings"
    26  
    27  	"github.com/pulumi/pulumi/pkg/v3/engine"
    28  	"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
    29  	"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
    30  	"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
    31  	"github.com/pulumi/pulumi/sdk/v3/go/common/display"
    32  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
    33  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
    34  	"github.com/sergi/go-diff/diffmatchpatch"
    35  	"gopkg.in/yaml.v3"
    36  )
    37  
    38  // getIndent computes a step's parent indentation.
    39  func getIndent(step engine.StepEventMetadata, seen map[resource.URN]engine.StepEventMetadata) int {
    40  	indent := 0
    41  	for p := step.Res.Parent; p != ""; {
    42  		if par, has := seen[p]; !has {
    43  			// This can happen during deletes, since we delete children before parents.
    44  			// TODO[pulumi/pulumi#340]: we need to figure out how best to display this sequence; at the very
    45  			//     least, it would be ideal to preserve the indentation.
    46  			break
    47  		} else {
    48  			indent++
    49  			p = par.Res.Parent
    50  		}
    51  	}
    52  	return indent
    53  }
    54  
    55  func printStepHeader(b io.StringWriter, step engine.StepEventMetadata) {
    56  	var extra string
    57  	old := step.Old
    58  	new := step.New
    59  	if new != nil && !new.Protect && old != nil && old.Protect {
    60  		// show an unlocked symbol, since we are unprotecting a resource.
    61  		extra = " 🔓"
    62  	} else if (new != nil && new.Protect) || (old != nil && old.Protect) {
    63  		// show a locked symbol, since we are either newly protecting this resource, or retaining protection.
    64  		extra = " 🔒"
    65  	}
    66  	writeString(b, fmt.Sprintf("%s: (%s)%s\n", string(step.Type), step.Op, extra))
    67  }
    68  
    69  func getIndentationString(indent int, op display.StepOp, prefix bool) string {
    70  	result := strings.Repeat("    ", indent)
    71  
    72  	if !prefix {
    73  		return result
    74  	}
    75  
    76  	if result == "" {
    77  		contract.Assertf(!prefix, "Expected indention for a prefixed line")
    78  		return result
    79  	}
    80  
    81  	rp := deploy.RawPrefix(op)
    82  	contract.Assert(len(rp) == 2)
    83  	contract.Assert(len(result) >= 2)
    84  	return result[:len(result)-2] + rp
    85  }
    86  
    87  func writeString(b io.StringWriter, s string) {
    88  	_, err := b.WriteString(s)
    89  	contract.IgnoreError(err)
    90  }
    91  
    92  func writeWithIndent(b io.StringWriter, indent int, op display.StepOp, prefix bool, format string, a ...interface{}) {
    93  	writeString(b, deploy.Color(op))
    94  	writeString(b, getIndentationString(indent, op, prefix))
    95  	writeString(b, fmt.Sprintf(format, a...))
    96  	writeString(b, colors.Reset)
    97  }
    98  
    99  func writeWithIndentNoPrefix(b io.StringWriter, indent int, op display.StepOp, format string, a ...interface{}) {
   100  	writeWithIndent(b, indent, op, false, format, a...)
   101  }
   102  
   103  func write(b io.StringWriter, op display.StepOp, format string, a ...interface{}) {
   104  	writeWithIndentNoPrefix(b, 0, op, format, a...)
   105  }
   106  
   107  func writeVerbatim(b io.StringWriter, op display.StepOp, value string) {
   108  	writeWithIndentNoPrefix(b, 0, op, "%s", value)
   109  }
   110  
   111  func getResourcePropertiesSummary(step engine.StepEventMetadata, indent int) string {
   112  	var b bytes.Buffer
   113  
   114  	op := step.Op
   115  	urn := step.URN
   116  	old := step.Old
   117  
   118  	// Print the indentation.
   119  	writeString(&b, getIndentationString(indent, op, false))
   120  
   121  	// First, print out the operation's prefix.
   122  	writeString(&b, deploy.Prefix(op, true /*done*/))
   123  
   124  	// Next, print the resource type (since it is easy on the eyes and can be quickly identified).
   125  	printStepHeader(&b, step)
   126  
   127  	// For these simple properties, print them as 'same' if they're just an update or replace.
   128  	simplePropOp := considerSameIfNotCreateOrDelete(op)
   129  
   130  	// Print out the URN and, if present, the ID, as "pseudo-properties" and indent them.
   131  	var id resource.ID
   132  	if old != nil {
   133  		id = old.ID
   134  	}
   135  
   136  	// Always print the ID, URN, and provider.
   137  	if id != "" {
   138  		writeWithIndentNoPrefix(&b, indent+1, simplePropOp, "[id=%s]\n", string(id))
   139  	}
   140  	if urn != "" {
   141  		writeWithIndentNoPrefix(&b, indent+1, simplePropOp, "[urn=%s]\n", urn)
   142  	}
   143  
   144  	if step.Provider != "" {
   145  		new := step.New
   146  		if old != nil && new != nil && old.Provider != new.Provider {
   147  			newProv, err := providers.ParseReference(new.Provider)
   148  			contract.Assert(err == nil)
   149  
   150  			writeWithIndentNoPrefix(&b, indent+1, deploy.OpUpdate, "[provider: ")
   151  			write(&b, deploy.OpDelete, "%s", old.Provider)
   152  			writeVerbatim(&b, deploy.OpUpdate, " => ")
   153  			if newProv.ID() == providers.UnknownID {
   154  				write(&b, deploy.OpCreate, "%s", string(newProv.URN())+"::output<string>")
   155  			} else {
   156  				write(&b, deploy.OpCreate, "%s", new.Provider)
   157  			}
   158  			writeVerbatim(&b, deploy.OpUpdate, "]\n")
   159  		} else {
   160  			prov, err := providers.ParseReference(step.Provider)
   161  			contract.Assert(err == nil)
   162  
   163  			// Elide references to default providers.
   164  			if prov.URN().Name() != "default" {
   165  				writeWithIndentNoPrefix(&b, indent+1, simplePropOp, "[provider=%s]\n", step.Provider)
   166  			}
   167  		}
   168  	}
   169  
   170  	return b.String()
   171  }
   172  
   173  func getResourcePropertiesDetails(
   174  	step engine.StepEventMetadata, indent int, planning bool, summary bool, truncateOutput bool, debug bool) string {
   175  	var b bytes.Buffer
   176  
   177  	// indent everything an additional level, like other properties.
   178  	indent++
   179  
   180  	old, new := step.Old, step.New
   181  	if old == nil && new != nil {
   182  		if len(new.Outputs) > 0 {
   183  			PrintObject(&b, new.Outputs, planning, indent, step.Op, false, truncateOutput, debug)
   184  		} else {
   185  			PrintObject(&b, new.Inputs, planning, indent, step.Op, false, truncateOutput, debug)
   186  		}
   187  	} else if new == nil && old != nil {
   188  		// in summary view, we don't have to print out the entire object that is getting deleted.
   189  		// note, the caller will have already printed out the type/name/id/urn of the resource,
   190  		// and that's sufficient for a summarized deletion view.
   191  		if !summary {
   192  			PrintObject(&b, old.Inputs, planning, indent, step.Op, false, truncateOutput, debug)
   193  		}
   194  	} else if len(new.Outputs) > 0 && step.Op != deploy.OpImport && step.Op != deploy.OpImportReplacement {
   195  		printOldNewDiffs(&b, old.Outputs, new.Outputs, nil, planning, indent, step.Op, summary, truncateOutput, debug)
   196  	} else {
   197  		printOldNewDiffs(&b, old.Inputs, new.Inputs, step.Diffs, planning, indent, step.Op, summary, truncateOutput, debug)
   198  	}
   199  
   200  	return b.String()
   201  }
   202  
   203  func maxKey(keys []resource.PropertyKey) int {
   204  	maxkey := 0
   205  	for _, k := range keys {
   206  		if len(k) > maxkey {
   207  			maxkey = len(k)
   208  		}
   209  	}
   210  	return maxkey
   211  }
   212  
   213  func PrintObject(
   214  	b *bytes.Buffer, props resource.PropertyMap, planning bool,
   215  	indent int, op display.StepOp, prefix bool, truncateOutput bool, debug bool) {
   216  
   217  	p := propertyPrinter{
   218  		dest:           b,
   219  		planning:       planning,
   220  		indent:         indent,
   221  		op:             op,
   222  		prefix:         prefix,
   223  		debug:          debug,
   224  		truncateOutput: truncateOutput,
   225  	}
   226  	p.printObject(props)
   227  }
   228  
   229  func (p *propertyPrinter) printObject(props resource.PropertyMap) {
   230  	// Compute the maximum width of property keys so we can justify everything.
   231  	keys := props.StableKeys()
   232  	maxkey := maxKey(keys)
   233  
   234  	// Now print out the values intelligently based on the type.
   235  	for _, k := range keys {
   236  		if v := props[k]; !resource.IsInternalPropertyKey(k) && shouldPrintPropertyValue(v, p.planning) {
   237  			p.printObjectProperty(k, v, maxkey)
   238  		}
   239  	}
   240  }
   241  
   242  func (p *propertyPrinter) printObjectProperty(key resource.PropertyKey, value resource.PropertyValue, maxkey int) {
   243  	p.printPropertyTitle(string(key), maxkey)
   244  	p.printPropertyValue(value)
   245  }
   246  
   247  func PrintResourceReference(
   248  	b *bytes.Buffer, resRef resource.ResourceReference, planning bool,
   249  	indent int, op display.StepOp, prefix bool, debug bool) {
   250  
   251  	p := propertyPrinter{
   252  		dest:     b,
   253  		planning: planning,
   254  		indent:   indent,
   255  		op:       op,
   256  		prefix:   prefix,
   257  		debug:    debug,
   258  	}
   259  	p.printResourceReference(resRef)
   260  }
   261  
   262  func (p *propertyPrinter) printResourceReference(resRef resource.ResourceReference) {
   263  	p.printPropertyTitle("URN", 3)
   264  	p.write("%q\n", resRef.URN)
   265  	p.printPropertyTitle("ID", 3)
   266  	p.printPropertyValue(resRef.ID)
   267  	p.printPropertyTitle("PackageVersion", 3)
   268  	p.write("%q\n", resRef.PackageVersion)
   269  }
   270  
   271  func massageStackPreviewAdd(p resource.PropertyValue) resource.PropertyValue {
   272  	switch {
   273  	case p.IsArray():
   274  		arr := make([]resource.PropertyValue, len(p.ArrayValue()))
   275  		for i, v := range p.ArrayValue() {
   276  			arr[i] = massageStackPreviewAdd(v)
   277  		}
   278  		return resource.NewArrayProperty(arr)
   279  	case p.IsObject():
   280  		obj := resource.PropertyMap{}
   281  		for k, v := range p.ObjectValue() {
   282  			if k != "@isPulumiResource" {
   283  				obj[k] = massageStackPreviewAdd(v)
   284  			}
   285  		}
   286  		return resource.NewObjectProperty(obj)
   287  	default:
   288  		return p
   289  	}
   290  }
   291  
   292  func massageStackPreviewDiff(diff resource.ValueDiff, inResource bool) {
   293  	switch {
   294  	case diff.Array != nil:
   295  		for i, p := range diff.Array.Adds {
   296  			diff.Array.Adds[i] = massageStackPreviewAdd(p)
   297  		}
   298  		for _, d := range diff.Array.Updates {
   299  			massageStackPreviewDiff(d, inResource)
   300  		}
   301  	case diff.Object != nil:
   302  		massageStackPreviewOutputDiff(diff.Object, inResource)
   303  	}
   304  }
   305  
   306  // massageStackPreviewOutputDiff removes any adds of unknown values nested inside Pulumi resources present in a stack's
   307  // outputs.
   308  func massageStackPreviewOutputDiff(diff *resource.ObjectDiff, inResource bool) {
   309  	if diff == nil {
   310  		return
   311  	}
   312  
   313  	_, isResource := diff.Adds["@isPulumiResource"]
   314  	if isResource {
   315  		delete(diff.Adds, "@isPulumiResource")
   316  
   317  		for k, v := range diff.Adds {
   318  			if v.IsComputed() {
   319  				delete(diff.Adds, k)
   320  			}
   321  		}
   322  	}
   323  
   324  	for i, p := range diff.Adds {
   325  		diff.Adds[i] = massageStackPreviewAdd(p)
   326  	}
   327  	for k, d := range diff.Updates {
   328  		if isResource && d.New.IsComputed() && !shouldPrintPropertyValue(d.Old, false) {
   329  			delete(diff.Updates, k)
   330  		} else {
   331  			massageStackPreviewDiff(d, inResource)
   332  		}
   333  	}
   334  }
   335  
   336  // getResourceOutputsPropertiesString prints only those properties that either differ from the input properties or, if
   337  // there is an old snapshot of the resource, differ from the prior old snapshot's output properties.
   338  func getResourceOutputsPropertiesString(
   339  	step engine.StepEventMetadata, indent int, planning, debug, refresh, showSames bool) string {
   340  
   341  	// During the actual update we always show all the outputs for the stack, even if they are unchanged.
   342  	if !showSames && !planning && step.URN.Type() == resource.RootStackType {
   343  		showSames = true
   344  	}
   345  
   346  	// We should only print outputs for normal resources if the outputs are known to be complete.
   347  	// This will be the case if we are:
   348  	//
   349  	//   1) not doing a preview
   350  	//   2) doing a refresh
   351  	//   3) doing a read
   352  	//   4) doing an import
   353  	//
   354  	// Technically, 2-4 are the same, since they're all bottoming out at a provider's implementation
   355  	// of Read, but the upshot is that either way we're ending up with outputs that are exactly
   356  	// accurate. If we are not sure that we are in one of the above states, we shouldn't try to
   357  	// print outputs.
   358  	//
   359  	// Note: we always show the outputs for the stack itself.  These are valuable enough to want
   360  	// to always see.
   361  	if planning {
   362  		printOutputDuringPlanning := refresh ||
   363  			step.Op == deploy.OpRead ||
   364  			step.Op == deploy.OpReadReplacement ||
   365  			step.Op == deploy.OpImport ||
   366  			step.Op == deploy.OpImportReplacement ||
   367  			step.URN.Type() == resource.RootStackType
   368  		if !printOutputDuringPlanning {
   369  			return ""
   370  		}
   371  	}
   372  
   373  	// Resources that have initialization errors did not successfully complete, and therefore do not
   374  	// have outputs to render diffs for. So, simply return.
   375  	if step.Old != nil && len(step.Old.InitErrors) > 0 {
   376  		return ""
   377  	}
   378  
   379  	// Only certain kinds of steps have output properties associated with them.
   380  	var ins resource.PropertyMap
   381  	var outs resource.PropertyMap
   382  	if step.New == nil || step.New.Outputs == nil {
   383  		ins = make(resource.PropertyMap)
   384  		outs = make(resource.PropertyMap)
   385  	} else {
   386  		ins = step.New.Inputs
   387  		outs = step.New.Outputs
   388  	}
   389  	op := step.Op
   390  
   391  	// If there was an old state associated with this step, we may have old outputs. If we do, and if they differ from
   392  	// the new outputs, we want to print the diffs.
   393  	var outputDiff *resource.ObjectDiff
   394  	if step.Old != nil && step.Old.Outputs != nil {
   395  		outputDiff = step.Old.Outputs.Diff(outs, resource.IsInternalPropertyKey)
   396  
   397  		// If this is the root stack type, we want to strip out any nested resource outputs that are not known if
   398  		// they have no corresponding output in the old state.
   399  		if planning && step.URN.Type() == resource.RootStackType {
   400  			massageStackPreviewOutputDiff(outputDiff, false)
   401  		}
   402  
   403  		// If we asked not to show-sames, and no outputs changed then don't show anything at all here.
   404  		if outputDiff == nil && !showSames {
   405  			return ""
   406  		}
   407  	}
   408  
   409  	var keys []resource.PropertyKey
   410  	if outputDiff == nil {
   411  		keys = outs.StableKeys()
   412  	} else {
   413  		keys = outputDiff.Keys()
   414  	}
   415  	maxkey := maxKey(keys)
   416  
   417  	b := &bytes.Buffer{}
   418  	p := propertyPrinter{
   419  		dest:     b,
   420  		planning: planning,
   421  		indent:   indent,
   422  		op:       op,
   423  		debug:    debug,
   424  	}
   425  
   426  	// Now sort the keys and enumerate each output property in a deterministic order.
   427  	for _, k := range keys {
   428  		out := outs[k]
   429  
   430  		// Print this property if it is printable and if any of the following are true:
   431  		// - a property with the same key is not present in the inputs
   432  		// - the property that is present in the inputs is different
   433  		// - we are doing a refresh, in which case we always want to show state differences
   434  		if outputDiff != nil || (!resource.IsInternalPropertyKey(k) && shouldPrintPropertyValue(out, true)) {
   435  			if in, has := ins[k]; has && !refresh {
   436  				if out.Diff(in, resource.IsInternalPropertyKey) == nil {
   437  					continue
   438  				}
   439  			}
   440  
   441  			// If we asked to not show-sames, and this is a same output, then filter it out of what
   442  			// we display.
   443  			if !showSames && outputDiff != nil && outputDiff.Same(k) {
   444  				continue
   445  			}
   446  
   447  			if outputDiff != nil {
   448  				p.printObjectPropertyDiff(k, maxkey, *outputDiff)
   449  			} else {
   450  				p.printObjectProperty(k, out, maxkey)
   451  			}
   452  		}
   453  	}
   454  
   455  	return b.String()
   456  }
   457  
   458  func considerSameIfNotCreateOrDelete(op display.StepOp) display.StepOp {
   459  	switch op {
   460  	case deploy.OpCreate, deploy.OpDelete, deploy.OpDeleteReplaced, deploy.OpReadDiscard, deploy.OpDiscardReplaced:
   461  		return op
   462  	default:
   463  		return deploy.OpSame
   464  	}
   465  }
   466  
   467  func shouldPrintPropertyValue(v resource.PropertyValue, outs bool) bool {
   468  	if v.IsNull() {
   469  		return false // don't print nulls (they just clutter up the output).
   470  	}
   471  	if v.IsString() && v.StringValue() == "" {
   472  		return false // don't print empty strings either.
   473  	}
   474  	if v.IsArray() && len(v.ArrayValue()) == 0 {
   475  		return false // skip empty arrays, since they are often uninteresting default values.
   476  	}
   477  	if v.IsObject() && len(v.ObjectValue()) == 0 {
   478  		return false // skip objects with no properties, since they are also uninteresting.
   479  	}
   480  	if v.IsObject() && len(v.ObjectValue()) == 0 {
   481  		return false // skip objects with no properties, since they are also uninteresting.
   482  	}
   483  	if v.IsOutput() && !outs {
   484  		// also don't show output properties until the outs parameter tells us to.
   485  		return false
   486  	}
   487  	return true
   488  }
   489  
   490  type propertyPrinter struct {
   491  	dest io.StringWriter
   492  
   493  	op             display.StepOp
   494  	planning       bool
   495  	prefix         bool
   496  	debug          bool
   497  	summary        bool
   498  	truncateOutput bool
   499  
   500  	indent int
   501  }
   502  
   503  func (p *propertyPrinter) indented(amt int) *propertyPrinter {
   504  	new := *p
   505  	new.indent += amt
   506  	return &new
   507  }
   508  
   509  func (p *propertyPrinter) withOp(op display.StepOp) *propertyPrinter {
   510  	new := *p
   511  	new.op = op
   512  	return &new
   513  }
   514  
   515  func (p *propertyPrinter) withPrefix(value bool) *propertyPrinter {
   516  	new := *p
   517  	new.prefix = value
   518  	return &new
   519  }
   520  
   521  func (p *propertyPrinter) writeString(s string) {
   522  	writeString(p.dest, s)
   523  }
   524  
   525  func (p *propertyPrinter) writeWithIndent(format string, a ...interface{}) {
   526  	if p.truncateOutput {
   527  		for i, item := range a {
   528  			if item, ok := item.(string); ok {
   529  				a[i] = p.truncatePropertyString(item)
   530  			}
   531  		}
   532  	}
   533  	writeWithIndent(p.dest, p.indent, p.op, p.prefix, format, a...)
   534  }
   535  
   536  func (p *propertyPrinter) writeWithIndentNoPrefix(format string, a ...interface{}) {
   537  	writeWithIndentNoPrefix(p.dest, p.indent, p.op, format, a...)
   538  }
   539  
   540  func (p *propertyPrinter) write(format string, a ...interface{}) {
   541  	write(p.dest, p.op, format, a...)
   542  }
   543  
   544  func (p *propertyPrinter) writeVerbatim(value string) {
   545  	writeVerbatim(p.dest, p.op, value)
   546  }
   547  
   548  func (p *propertyPrinter) printPropertyTitle(name string, align int) {
   549  	p.writeWithIndent("%-"+strconv.Itoa(align)+"s: ", name)
   550  }
   551  
   552  func propertyTitlePrinter(name string, align int) func(*propertyPrinter) {
   553  	return func(p *propertyPrinter) {
   554  		p.printPropertyTitle(name, align)
   555  	}
   556  }
   557  
   558  func (p *propertyPrinter) printPropertyValue(v resource.PropertyValue) {
   559  	switch {
   560  	case isPrimitive(v):
   561  		p.printPrimitivePropertyValue(v)
   562  	case v.IsArray():
   563  		arr := v.ArrayValue()
   564  		if len(arr) == 0 {
   565  			p.writeVerbatim("[]")
   566  		} else {
   567  			p.writeVerbatim("[\n")
   568  			for i, elem := range arr {
   569  				p.writeWithIndent("    [%d]: ", i)
   570  				p.indented(1).printPropertyValue(elem)
   571  			}
   572  			p.writeWithIndentNoPrefix("]")
   573  		}
   574  	case v.IsAsset():
   575  		a := v.AssetValue()
   576  		if a.IsText() {
   577  			p.write("asset(text:%s) {\n", shortHash(a.Hash))
   578  
   579  			a = resource.MassageIfUserProgramCodeAsset(a, p.debug)
   580  
   581  			massaged := a.Text
   582  
   583  			// pretty print the text, line by line, with proper breaks.
   584  			lines := strings.Split(massaged, "\n")
   585  			for _, line := range lines {
   586  				p.writeWithIndentNoPrefix("    %s\n", line)
   587  			}
   588  			p.writeWithIndentNoPrefix("}")
   589  		} else if path, has := a.GetPath(); has {
   590  			p.write("asset(file:%s) { %s }", shortHash(a.Hash), path)
   591  		} else {
   592  			contract.Assert(a.IsURI())
   593  			p.write("asset(uri:%s) { %s }", shortHash(a.Hash), a.URI)
   594  		}
   595  	case v.IsArchive():
   596  		a := v.ArchiveValue()
   597  		if assets, has := a.GetAssets(); has {
   598  			p.write("archive(assets:%s) {\n", shortHash(a.Hash))
   599  			var names []string
   600  			for name := range assets {
   601  				names = append(names, name)
   602  			}
   603  			sort.Strings(names)
   604  			for _, name := range names {
   605  				p.printAssetOrArchive(assets[name], name)
   606  			}
   607  			p.writeWithIndentNoPrefix("}")
   608  		} else if path, has := a.GetPath(); has {
   609  			p.write("archive(file:%s) { %s }", shortHash(a.Hash), path)
   610  		} else {
   611  			contract.Assert(a.IsURI())
   612  			p.write("archive(uri:%s) { %v }", shortHash(a.Hash), a.URI)
   613  		}
   614  	case v.IsObject():
   615  		obj := v.ObjectValue()
   616  		if len(obj) == 0 {
   617  			p.writeVerbatim("{}")
   618  		} else {
   619  			p.writeVerbatim("{\n")
   620  			p.indented(1).printObject(obj)
   621  			p.writeWithIndentNoPrefix("}")
   622  		}
   623  	case v.IsResourceReference():
   624  		resRef := v.ResourceReferenceValue()
   625  		p.writeVerbatim("{\n")
   626  		p.indented(1).printResourceReference(resRef)
   627  		p.writeWithIndentNoPrefix("}")
   628  	default:
   629  		contract.Failf("Unknown PropertyValue type %v", v)
   630  	}
   631  	p.writeVerbatim("\n")
   632  }
   633  
   634  func (p *propertyPrinter) printAssetOrArchive(v interface{}, name string) {
   635  	p.writeWithIndent("    \"%v\": ", name)
   636  	p.indented(1).printPropertyValue(assetOrArchiveToPropertyValue(v))
   637  }
   638  
   639  func assetOrArchiveToPropertyValue(v interface{}) resource.PropertyValue {
   640  	switch t := v.(type) {
   641  	case *resource.Asset:
   642  		return resource.NewAssetProperty(t)
   643  	case *resource.Archive:
   644  		return resource.NewArchiveProperty(t)
   645  	default:
   646  		contract.Failf("Unexpected archive element '%v'", reflect.TypeOf(t))
   647  		return resource.PropertyValue{V: nil}
   648  	}
   649  }
   650  
   651  func shortHash(hash string) string {
   652  	if len(hash) > 7 {
   653  		return hash[:7]
   654  	}
   655  	return hash
   656  }
   657  
   658  func printOldNewDiffs(
   659  	b *bytes.Buffer, olds resource.PropertyMap, news resource.PropertyMap, include []resource.PropertyKey,
   660  	planning bool, indent int, op display.StepOp, summary bool, truncateOutput bool, debug bool) {
   661  
   662  	// Get the full diff structure between the two, and print it (recursively).
   663  	if diff := olds.Diff(news, resource.IsInternalPropertyKey); diff != nil {
   664  		PrintObjectDiff(b, *diff, include, planning, indent, summary, truncateOutput, debug)
   665  	} else {
   666  		// If there's no diff, report the op as Same - there's no diff to render
   667  		// so it should be rendered as if nothing changed.
   668  		PrintObject(b, news, planning, indent, deploy.OpSame, true, truncateOutput, debug)
   669  	}
   670  }
   671  
   672  func PrintObjectDiff(b *bytes.Buffer, diff resource.ObjectDiff, include []resource.PropertyKey,
   673  	planning bool, indent int, summary bool, truncateOutput bool, debug bool) {
   674  
   675  	p := propertyPrinter{
   676  		dest:           b,
   677  		planning:       planning,
   678  		indent:         indent,
   679  		prefix:         true,
   680  		debug:          debug,
   681  		summary:        summary,
   682  		truncateOutput: truncateOutput,
   683  	}
   684  	p.printObjectDiff(diff, include)
   685  }
   686  
   687  func (p *propertyPrinter) printObjectDiff(diff resource.ObjectDiff, include []resource.PropertyKey) {
   688  	contract.Assert(p.indent > 0)
   689  
   690  	// Compute the maximum width of property keys so we can justify everything. If an include set was given, filter out
   691  	// any properties that are not in the set.
   692  	keys := diff.Keys()
   693  	if include != nil {
   694  		includeSet := make(map[resource.PropertyKey]bool)
   695  		for _, k := range include {
   696  			includeSet[k] = true
   697  		}
   698  		var filteredKeys []resource.PropertyKey
   699  		for _, k := range keys {
   700  			if includeSet[k] {
   701  				filteredKeys = append(filteredKeys, k)
   702  			}
   703  		}
   704  		keys = filteredKeys
   705  	}
   706  	maxkey := maxKey(keys)
   707  
   708  	// To print an object diff, enumerate the keys in stable order, and print each property independently.
   709  	for _, k := range keys {
   710  		p.printObjectPropertyDiff(k, maxkey, diff)
   711  	}
   712  }
   713  
   714  func (p *propertyPrinter) printObjectPropertyDiff(key resource.PropertyKey, maxkey int, diff resource.ObjectDiff) {
   715  	titleFunc := propertyTitlePrinter(string(key), maxkey)
   716  	if add, isadd := diff.Adds[key]; isadd {
   717  		p.printAdd(add, titleFunc)
   718  	} else if delete, isdelete := diff.Deletes[key]; isdelete {
   719  		p.printDelete(delete, titleFunc)
   720  	} else if update, isupdate := diff.Updates[key]; isupdate {
   721  		p.printPropertyValueDiff(titleFunc, update)
   722  	} else if same := diff.Sames[key]; !p.summary && shouldPrintPropertyValue(same, p.planning) {
   723  		p.withOp(deploy.OpSame).withPrefix(false).printObjectProperty(key, same, maxkey)
   724  	}
   725  }
   726  
   727  func (p *propertyPrinter) printPropertyValueDiff(titleFunc func(*propertyPrinter), diff resource.ValueDiff) {
   728  	p = p.withOp(deploy.OpUpdate).withPrefix(true)
   729  	contract.Assert(p.indent > 0)
   730  
   731  	if diff.Array != nil {
   732  		titleFunc(p)
   733  		p.writeVerbatim("[\n")
   734  
   735  		a := diff.Array
   736  		for i := 0; i < a.Len(); i++ {
   737  			elemPrinter := p.indented(2)
   738  			elemTitleFunc := func(p *propertyPrinter) {
   739  				p.indented(-1).writeWithIndent("[%d]: ", i)
   740  			}
   741  
   742  			if add, isadd := a.Adds[i]; isadd {
   743  				elemPrinter.printAdd(add, elemTitleFunc)
   744  			} else if delete, isdelete := a.Deletes[i]; isdelete {
   745  				elemPrinter.printDelete(delete, elemTitleFunc)
   746  			} else if update, isupdate := a.Updates[i]; isupdate {
   747  				elemPrinter.printPropertyValueDiff(elemTitleFunc, update)
   748  			} else if same, issame := a.Sames[i]; issame && !p.summary {
   749  				elemPrinter = elemPrinter.withOp(deploy.OpSame).withPrefix(false)
   750  				elemTitleFunc(elemPrinter)
   751  				elemPrinter.printPropertyValue(same)
   752  			}
   753  		}
   754  		p.writeWithIndentNoPrefix("]\n")
   755  	} else if diff.Object != nil {
   756  		titleFunc(p)
   757  		p.writeVerbatim("{\n")
   758  		p.indented(1).printObjectDiff(*diff.Object, nil)
   759  		p.writeWithIndentNoPrefix("}\n")
   760  	} else {
   761  		shouldPrintOld := shouldPrintPropertyValue(diff.Old, false)
   762  		shouldPrintNew := shouldPrintPropertyValue(diff.New, false)
   763  
   764  		if shouldPrintOld && shouldPrintNew {
   765  			if diff.Old.IsArchive() &&
   766  				diff.New.IsArchive() {
   767  
   768  				p.printArchiveDiff(titleFunc, diff.Old.ArchiveValue(), diff.New.ArchiveValue())
   769  				return
   770  			}
   771  
   772  			if isPrimitive(diff.Old) && isPrimitive(diff.New) {
   773  				titleFunc(p)
   774  
   775  				if diff.Old.IsString() && diff.New.IsString() {
   776  					p.printTextDiff(diff.Old.StringValue(), diff.New.StringValue())
   777  					return
   778  				}
   779  
   780  				p.withOp(deploy.OpDelete).printPrimitivePropertyValue(diff.Old)
   781  				p.writeVerbatim(" => ")
   782  				p.withOp(deploy.OpCreate).printPrimitivePropertyValue(diff.New)
   783  				p.writeVerbatim("\n")
   784  				return
   785  			}
   786  		}
   787  
   788  		// If we ended up here, the two values either differ by type, or they have different primitive values.  We will
   789  		// simply emit a deletion line followed by an addition line.
   790  		if shouldPrintOld {
   791  			p.printDelete(diff.Old, titleFunc)
   792  		}
   793  		if shouldPrintNew {
   794  			p.printAdd(diff.New, titleFunc)
   795  		}
   796  	}
   797  }
   798  
   799  func isPrimitive(value resource.PropertyValue) bool {
   800  	return value.IsNull() || value.IsString() || value.IsNumber() ||
   801  		value.IsBool() || value.IsComputed() || value.IsOutput() || value.IsSecret()
   802  }
   803  
   804  func (p *propertyPrinter) printPrimitivePropertyValue(v resource.PropertyValue) {
   805  	contract.Assert(isPrimitive(v))
   806  	if v.IsNull() {
   807  		p.writeVerbatim("<null>")
   808  	} else if v.IsBool() {
   809  		p.write("%t", v.BoolValue())
   810  	} else if v.IsNumber() {
   811  		p.write("%v", v.NumberValue())
   812  	} else if v.IsString() {
   813  		if vv, kind, ok := p.decodeValue(v.StringValue()); ok {
   814  			p.write("(%s) ", kind)
   815  			p.printPropertyValue(vv)
   816  			return
   817  		}
   818  		if p.truncateOutput {
   819  			p.write("%q", p.truncatePropertyString(v.StringValue()))
   820  		} else {
   821  			p.write("%q", v.StringValue())
   822  		}
   823  	} else if v.IsComputed() || v.IsOutput() {
   824  		// We render computed and output values differently depending on whether or not we are
   825  		// planning or deploying: in the former case, we display `computed<type>` or `output<type>`;
   826  		// in the former we display `undefined`. This is because we currently cannot distinguish
   827  		// between user-supplied undefined values and input properties that are undefined because
   828  		// they were sourced from undefined values in other resources' output properties. Once we
   829  		// have richer information about the dataflow between resources, we should be able to do a
   830  		// better job here (pulumi/pulumi#234).
   831  		if p.planning {
   832  			p.writeVerbatim(v.TypeString())
   833  		} else {
   834  			p.write("undefined")
   835  		}
   836  	} else if v.IsSecret() {
   837  		p.write("[secret]")
   838  	} else {
   839  		contract.Failf("Unexpected property value kind '%v'", v)
   840  	}
   841  }
   842  
   843  func (p *propertyPrinter) printDelete(v resource.PropertyValue, title func(*propertyPrinter)) {
   844  	p = p.withOp(deploy.OpDelete).withPrefix(true)
   845  	title(p)
   846  	p.printPropertyValue(v)
   847  }
   848  
   849  func (p *propertyPrinter) printAdd(v resource.PropertyValue, title func(*propertyPrinter)) {
   850  	p = p.withOp(deploy.OpCreate).withPrefix(true)
   851  	title(p)
   852  	p.printPropertyValue(v)
   853  }
   854  
   855  func (p *propertyPrinter) printArchiveDiff(titleFunc func(*propertyPrinter),
   856  	oldArchive, newArchive *resource.Archive) {
   857  
   858  	p = p.withOp(deploy.OpUpdate).withPrefix(true)
   859  
   860  	hashChange := getTextChangeString(shortHash(oldArchive.Hash), shortHash(newArchive.Hash))
   861  
   862  	if oldPath, has := oldArchive.GetPath(); has {
   863  		if newPath, has := newArchive.GetPath(); has {
   864  			titleFunc(p)
   865  			p.write("archive(file:%s) { %s }\n", hashChange, getTextChangeString(oldPath, newPath))
   866  			return
   867  		}
   868  	} else if oldURI, has := oldArchive.GetURI(); has {
   869  		if newURI, has := newArchive.GetURI(); has {
   870  			titleFunc(p)
   871  			p.write("archive(uri:%s) { %s }\n", hashChange, getTextChangeString(oldURI, newURI))
   872  			return
   873  		}
   874  	} else {
   875  		contract.Assert(oldArchive.IsAssets())
   876  		oldAssets, _ := oldArchive.GetAssets()
   877  
   878  		if newAssets, has := newArchive.GetAssets(); has {
   879  			titleFunc(p)
   880  			p.write("archive(assets:%s) {\n", hashChange)
   881  			p.indented(1).printAssetsDiff(oldAssets, newAssets)
   882  			p.writeWithIndentNoPrefix("}\n")
   883  			return
   884  		}
   885  	}
   886  
   887  	// Type of archive changed, print this out as an remove and an add.
   888  	p.printDelete(assetOrArchiveToPropertyValue(oldArchive), titleFunc)
   889  	p.printAdd(assetOrArchiveToPropertyValue(newArchive), titleFunc)
   890  }
   891  
   892  func (p *propertyPrinter) printAssetsDiff(oldAssets, newAssets map[string]interface{}) {
   893  	// Diffing assets proceeds by getting the sorted list of asset names from both the old and
   894  	// new assets, and then stepwise processing each.  For any asset in old that isn't in new,
   895  	// we print this out as a delete.  For any asset in new that isn't in old, we print this out
   896  	// as an add.  For any asset in both we print out of it is unchanged or not.  If so, we
   897  	// recurse on that data to print out how it changed.
   898  
   899  	var oldNames []string
   900  	var newNames []string
   901  
   902  	for name := range oldAssets {
   903  		oldNames = append(oldNames, name)
   904  	}
   905  
   906  	for name := range newAssets {
   907  		newNames = append(newNames, name)
   908  	}
   909  
   910  	sort.Strings(oldNames)
   911  	sort.Strings(newNames)
   912  
   913  	i := 0
   914  	j := 0
   915  
   916  	var keys []resource.PropertyKey
   917  	for _, name := range oldNames {
   918  		keys = append(keys, "\""+resource.PropertyKey(name)+"\"")
   919  	}
   920  	for _, name := range newNames {
   921  		keys = append(keys, "\""+resource.PropertyKey(name)+"\"")
   922  	}
   923  
   924  	maxkey := maxKey(keys)
   925  
   926  	for i < len(oldNames) || j < len(newNames) {
   927  		deleteOld := false
   928  		addNew := false
   929  		if i < len(oldNames) && j < len(newNames) {
   930  			oldName := oldNames[i]
   931  			newName := newNames[j]
   932  
   933  			if oldName == newName {
   934  				titleFunc := propertyTitlePrinter("\""+oldName+"\"", maxkey)
   935  
   936  				old := oldAssets[oldName]
   937  				new := newAssets[newName]
   938  
   939  				// If the assets/archvies haven't changed, then don't bother printing them out.
   940  				// This happens routinely when we have an archive that has changed because some
   941  				// asset it in it changed.  We want *that* asset to be printed, but not all the
   942  				// unchanged assets.
   943  
   944  				switch t := old.(type) {
   945  				case *resource.Archive:
   946  					newArchive, newIsArchive := new.(*resource.Archive)
   947  					switch {
   948  					case !newIsArchive:
   949  						p.printAssetArchiveDiff(titleFunc, t, new)
   950  					case t.Hash != newArchive.Hash:
   951  						p.printArchiveDiff(titleFunc, t, newArchive)
   952  					}
   953  				case *resource.Asset:
   954  					newAsset, newIsAsset := new.(*resource.Asset)
   955  					switch {
   956  					case !newIsAsset:
   957  						p.printAssetArchiveDiff(titleFunc, t, new)
   958  					case t.Hash != newAsset.Hash:
   959  						p.printAssetDiff(titleFunc, t, newAsset)
   960  					}
   961  				}
   962  
   963  				i++
   964  				j++
   965  				continue
   966  			}
   967  
   968  			if oldName < newName {
   969  				deleteOld = true
   970  			} else {
   971  				addNew = true
   972  			}
   973  		} else if i < len(oldNames) {
   974  			deleteOld = true
   975  		} else {
   976  			addNew = true
   977  		}
   978  
   979  		if deleteOld {
   980  			oldName := oldNames[i]
   981  			titleFunc := propertyTitlePrinter("\""+oldName+"\"", maxkey)
   982  			p.indented(1).printDelete(assetOrArchiveToPropertyValue(oldAssets[oldName]), titleFunc)
   983  			i++
   984  			continue
   985  		} else {
   986  			contract.Assert(addNew)
   987  			newName := newNames[j]
   988  			titleFunc := propertyTitlePrinter("\""+newName+"\"", maxkey)
   989  			p.indented(1).printAdd(assetOrArchiveToPropertyValue(newAssets[newName]), titleFunc)
   990  			j++
   991  		}
   992  	}
   993  }
   994  
   995  func (p *propertyPrinter) printAssetDiff(titleFunc func(*propertyPrinter), oldAsset, newAsset *resource.Asset) {
   996  	contract.Assertf(oldAsset.Hash != newAsset.Hash, "Should not call printAssetDiff on unchanged assets")
   997  
   998  	p = p.withOp(deploy.OpUpdate).withPrefix(true)
   999  
  1000  	// if the asset changed, print out: ~ assetName: type(hash->hash) details...
  1001  	hashChange := getTextChangeString(shortHash(oldAsset.Hash), shortHash(newAsset.Hash))
  1002  
  1003  	if oldAsset.IsText() {
  1004  		if newAsset.IsText() {
  1005  			titleFunc(p)
  1006  			p.write("asset(text:%s) {", hashChange)
  1007  
  1008  			massagedOldText := resource.MassageIfUserProgramCodeAsset(oldAsset, p.debug).Text
  1009  			massagedNewText := resource.MassageIfUserProgramCodeAsset(newAsset, p.debug).Text
  1010  
  1011  			p.indented(1).printTextDiff(massagedOldText, massagedNewText)
  1012  
  1013  			p.writeWithIndentNoPrefix("}\n")
  1014  			return
  1015  		}
  1016  	} else if oldPath, has := oldAsset.GetPath(); has {
  1017  		if newPath, has := newAsset.GetPath(); has {
  1018  			titleFunc(p)
  1019  			p.write("asset(file:%s) { %s }\n", hashChange, getTextChangeString(oldPath, newPath))
  1020  			return
  1021  		}
  1022  	} else {
  1023  		contract.Assert(oldAsset.IsURI())
  1024  
  1025  		oldURI, _ := oldAsset.GetURI()
  1026  		if newURI, has := newAsset.GetURI(); has {
  1027  			titleFunc(p)
  1028  			p.write("asset(uri:%s) { %s }\n", hashChange, getTextChangeString(oldURI, newURI))
  1029  			return
  1030  		}
  1031  	}
  1032  
  1033  	// Type of asset changed, print this out as an remove and an add.
  1034  	p.printDelete(assetOrArchiveToPropertyValue(oldAsset), titleFunc)
  1035  	p.printAdd(assetOrArchiveToPropertyValue(newAsset), titleFunc)
  1036  }
  1037  
  1038  func (p *propertyPrinter) printAssetArchiveDiff(titleFunc func(p *propertyPrinter), old, new interface{}) {
  1039  	p.printDelete(assetOrArchiveToPropertyValue(old), titleFunc)
  1040  	p.printAdd(assetOrArchiveToPropertyValue(new), titleFunc)
  1041  }
  1042  
  1043  func getTextChangeString(old string, new string) string {
  1044  	if old == new {
  1045  		return old
  1046  	}
  1047  
  1048  	return fmt.Sprintf("%s->%s", old, new)
  1049  }
  1050  
  1051  func escape(s string) string {
  1052  	escaped := strconv.Quote(s)
  1053  	return escaped[1 : len(escaped)-1]
  1054  }
  1055  
  1056  func (p *propertyPrinter) printTextDiff(old, new string) {
  1057  	if p.printEncodedValueDiff(old, new) {
  1058  		return
  1059  	}
  1060  
  1061  	differ := diffmatchpatch.New()
  1062  	differ.DiffTimeout = 0
  1063  
  1064  	singleLine := !strings.ContainsRune(old, '\n') && !strings.ContainsRune(new, '\n')
  1065  	if singleLine {
  1066  		diff := differ.DiffMain(old, new, false)
  1067  		p.printCharacterDiff(differ.DiffCleanupEfficiency(diff))
  1068  	} else {
  1069  		hashed1, hashed2, lineArray := differ.DiffLinesToChars(old, new)
  1070  		diffs := differ.DiffMain(hashed1, hashed2, false)
  1071  		p.indented(1).printLineDiff(differ.DiffCharsToLines(diffs, lineArray))
  1072  	}
  1073  }
  1074  
  1075  func (p *propertyPrinter) printCharacterDiff(diffs []diffmatchpatch.Diff) {
  1076  	// write the old text.
  1077  	p.writeVerbatim(`"`)
  1078  	for _, d := range diffs {
  1079  		switch d.Type {
  1080  		case diffmatchpatch.DiffDelete:
  1081  			p.withOp(deploy.OpDelete).write(escape(d.Text))
  1082  		case diffmatchpatch.DiffEqual:
  1083  			p.withOp(deploy.OpSame).write(escape(d.Text))
  1084  		}
  1085  	}
  1086  	p.writeVerbatim(`"`)
  1087  
  1088  	p.writeVerbatim(" => ")
  1089  
  1090  	// write the new text.
  1091  	p.writeVerbatim(`"`)
  1092  	for _, d := range diffs {
  1093  		switch d.Type {
  1094  		case diffmatchpatch.DiffInsert:
  1095  			p.withOp(deploy.OpCreate).write(escape(d.Text))
  1096  		case diffmatchpatch.DiffEqual:
  1097  			p.withOp(deploy.OpSame).write(escape(d.Text))
  1098  		}
  1099  	}
  1100  	p.writeVerbatim("\"\n")
  1101  }
  1102  
  1103  // printLineDiff takes the full diff produed by diffmatchpatch and condenses it into something
  1104  // useful we can print to the console. Specifically, while it includes any adds/removes in
  1105  // green/red, it will also show portions of the unchanged text to help give surrounding context to
  1106  // those add/removes. Because the unchanged portions may be very large, it only included around 3
  1107  // lines before/after the change.
  1108  func (p *propertyPrinter) printLineDiff(diffs []diffmatchpatch.Diff) {
  1109  	p.writeVerbatim("\n")
  1110  
  1111  	writeDiff := func(op display.StepOp, text string) {
  1112  		prefix := op == deploy.OpCreate || op == deploy.OpDelete
  1113  		p.withOp(op).withPrefix(prefix).writeWithIndent("%s", text)
  1114  	}
  1115  
  1116  	for index, diff := range diffs {
  1117  		text := diff.Text
  1118  		lines := strings.Split(text, "\n")
  1119  		printLines := func(op display.StepOp, startInclusive int, endExclusive int) {
  1120  			for i := startInclusive; i < endExclusive; i++ {
  1121  				if strings.TrimSpace(lines[i]) != "" {
  1122  					writeDiff(op, lines[i])
  1123  					p.writeString("\n")
  1124  				}
  1125  			}
  1126  		}
  1127  
  1128  		switch diff.Type {
  1129  		case diffmatchpatch.DiffInsert:
  1130  			printLines(deploy.OpCreate, 0, len(lines))
  1131  		case diffmatchpatch.DiffDelete:
  1132  			printLines(deploy.OpDelete, 0, len(lines))
  1133  		case diffmatchpatch.DiffEqual:
  1134  			var trimmedLines []string
  1135  			for _, line := range lines {
  1136  				if strings.TrimSpace(line) != "" {
  1137  					trimmedLines = append(trimmedLines, line)
  1138  				}
  1139  			}
  1140  			lines = trimmedLines
  1141  
  1142  			const contextLines = 2
  1143  
  1144  			// Show the unchanged text in white.
  1145  			if index == 0 {
  1146  				// First chunk of the file.
  1147  				if len(lines) > contextLines+1 {
  1148  					writeDiff(deploy.OpSame, "...\n")
  1149  					printLines(deploy.OpSame, len(lines)-contextLines, len(lines))
  1150  					continue
  1151  				}
  1152  			} else if index == len(diffs)-1 {
  1153  				if len(lines) > contextLines+1 {
  1154  					printLines(deploy.OpSame, 0, contextLines)
  1155  					writeDiff(deploy.OpSame, "...\n")
  1156  					continue
  1157  				}
  1158  			} else {
  1159  				if len(lines) > (2*contextLines + 1) {
  1160  					printLines(deploy.OpSame, 0, contextLines)
  1161  					writeDiff(deploy.OpSame, "...\n")
  1162  					printLines(deploy.OpSame, len(lines)-contextLines, len(lines))
  1163  					continue
  1164  				}
  1165  			}
  1166  
  1167  			printLines(deploy.OpSame, 0, len(lines))
  1168  		}
  1169  	}
  1170  }
  1171  
  1172  func (p *propertyPrinter) printEncodedValueDiff(old, new string) bool {
  1173  	oldValue, oldKind, ok := p.decodeValue(old)
  1174  	if !ok {
  1175  		return false
  1176  	}
  1177  
  1178  	newValue, newKind, ok := p.decodeValue(new)
  1179  	if !ok {
  1180  		return false
  1181  	}
  1182  
  1183  	if oldKind == newKind {
  1184  		p.write("(%s) ", oldKind)
  1185  	} else {
  1186  		p.write("(%s => %s) ", oldKind, newKind)
  1187  	}
  1188  
  1189  	diff := oldValue.Diff(newValue, resource.IsInternalPropertyKey)
  1190  	if diff == nil {
  1191  		p.withOp(deploy.OpSame).printPropertyValue(oldValue)
  1192  		return true
  1193  	}
  1194  
  1195  	p.printPropertyValueDiff(func(*propertyPrinter) {}, *diff)
  1196  	return true
  1197  }
  1198  
  1199  func (p *propertyPrinter) decodeValue(repr string) (resource.PropertyValue, string, bool) {
  1200  	decode := func() (interface{}, string, bool) {
  1201  		r := strings.NewReader(repr)
  1202  
  1203  		var object interface{}
  1204  		if err := json.NewDecoder(r).Decode(&object); err == nil {
  1205  			return object, "json", true
  1206  		}
  1207  
  1208  		r.Reset(repr)
  1209  		if err := yaml.NewDecoder(r).Decode(&object); err == nil {
  1210  			translated, ok := p.translateYAMLValue(object)
  1211  			if !ok {
  1212  				return nil, "", false
  1213  			}
  1214  			return translated, "yaml", true
  1215  		}
  1216  
  1217  		return nil, "", false
  1218  	}
  1219  
  1220  	object, kind, ok := decode()
  1221  	if ok {
  1222  		switch object.(type) {
  1223  		case []interface{}, map[string]interface{}:
  1224  			return resource.NewPropertyValue(object), kind, true
  1225  		}
  1226  	}
  1227  	return resource.PropertyValue{}, "", false
  1228  }
  1229  
  1230  // translateYAMLValue attempts to replace map[interface{}]interface{} values in a decoded YAML value with
  1231  // map[string]interface{} values. map[interface{}]interface{} values can arise from YAML mappings with keys that are
  1232  // not strings. This method only translates such maps if they have purely numeric keys--maps with slice or map keys
  1233  // are not translated.
  1234  func (p *propertyPrinter) translateYAMLValue(v interface{}) (interface{}, bool) {
  1235  	switch v := v.(type) {
  1236  	case []interface{}:
  1237  		for i, e := range v {
  1238  			ee, ok := p.translateYAMLValue(e)
  1239  			if !ok {
  1240  				return nil, false
  1241  			}
  1242  			v[i] = ee
  1243  		}
  1244  		return v, true
  1245  	case map[string]interface{}:
  1246  		for k, e := range v {
  1247  			ee, ok := p.translateYAMLValue(e)
  1248  			if !ok {
  1249  				return nil, false
  1250  			}
  1251  			v[k] = ee
  1252  		}
  1253  		return v, true
  1254  	case map[interface{}]interface{}:
  1255  		vv := make(map[string]interface{}, len(v))
  1256  		for k, e := range v {
  1257  			sk := ""
  1258  			switch k := k.(type) {
  1259  			case string:
  1260  				sk = k
  1261  			case int:
  1262  				sk = strconv.FormatInt(int64(k), 10)
  1263  			case int64:
  1264  				sk = strconv.FormatInt(k, 10)
  1265  			case uint64:
  1266  				sk = strconv.FormatUint(k, 10)
  1267  			case float64:
  1268  				sk = strconv.FormatFloat(k, 'g', -1, 64)
  1269  			default:
  1270  				return nil, false
  1271  			}
  1272  
  1273  			ee, ok := p.translateYAMLValue(e)
  1274  			if !ok {
  1275  				return nil, false
  1276  			}
  1277  			vv[sk] = ee
  1278  		}
  1279  		return vv, true
  1280  	default:
  1281  		return v, true
  1282  	}
  1283  }
  1284  
  1285  // if string exceeds three lines or is >150 characters, truncate and add "..."
  1286  func (p *propertyPrinter) truncatePropertyString(propertyString string) string {
  1287  	const (
  1288  		contextLines  = 3
  1289  		maxLineLength = 150
  1290  	)
  1291  
  1292  	lines := strings.Split(propertyString, "\n")
  1293  	numLines := len(lines)
  1294  	if numLines > contextLines {
  1295  		numLines = contextLines
  1296  	}
  1297  
  1298  	isTruncated := false
  1299  	for i := 0; i < numLines; i++ {
  1300  		if len(lines[i]) > maxLineLength {
  1301  			lines[i] = lines[i][:maxLineLength] + "..."
  1302  			isTruncated = true
  1303  		}
  1304  	}
  1305  
  1306  	if !isTruncated {
  1307  		return propertyString
  1308  	}
  1309  
  1310  	if len(lines) <= contextLines {
  1311  		return strings.Join(lines, "\n")
  1312  	}
  1313  
  1314  	return strings.Join(lines[:numLines], "\n") + "\n..."
  1315  }