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