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