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