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