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