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