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