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 }