github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/command/format/diff.go (about) 1 package format 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "sort" 8 "strings" 9 10 "github.com/mitchellh/colorstring" 11 "github.com/zclconf/go-cty/cty" 12 ctyjson "github.com/zclconf/go-cty/cty/json" 13 14 "github.com/hashicorp/terraform-plugin-sdk/internal/addrs" 15 "github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema" 16 "github.com/hashicorp/terraform-plugin-sdk/internal/plans" 17 "github.com/hashicorp/terraform-plugin-sdk/internal/plans/objchange" 18 "github.com/hashicorp/terraform-plugin-sdk/internal/states" 19 ) 20 21 // ResourceChange returns a string representation of a change to a particular 22 // resource, for inclusion in user-facing plan output. 23 // 24 // The resource schema must be provided along with the change so that the 25 // formatted change can reflect the configuration structure for the associated 26 // resource. 27 // 28 // If "color" is non-nil, it will be used to color the result. Otherwise, 29 // no color codes will be included. 30 func ResourceChange( 31 change *plans.ResourceInstanceChangeSrc, 32 tainted bool, 33 schema *configschema.Block, 34 color *colorstring.Colorize, 35 ) string { 36 addr := change.Addr 37 var buf bytes.Buffer 38 39 if color == nil { 40 color = &colorstring.Colorize{ 41 Colors: colorstring.DefaultColors, 42 Disable: true, 43 Reset: false, 44 } 45 } 46 47 dispAddr := addr.String() 48 if change.DeposedKey != states.NotDeposed { 49 dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, change.DeposedKey) 50 } 51 52 switch change.Action { 53 case plans.Create: 54 buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be created", dispAddr))) 55 case plans.Read: 56 buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be read during apply\n # (config refers to values not yet known)", dispAddr))) 57 case plans.Update: 58 buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be updated in-place", dispAddr))) 59 case plans.CreateThenDelete, plans.DeleteThenCreate: 60 if tainted { 61 buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] is tainted, so must be [bold][red]replaced", dispAddr))) 62 } else { 63 buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] must be [bold][red]replaced", dispAddr))) 64 } 65 case plans.Delete: 66 buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]destroyed", dispAddr))) 67 default: 68 // should never happen, since the above is exhaustive 69 buf.WriteString(fmt.Sprintf("%s has an action the plan renderer doesn't support (this is a bug)", dispAddr)) 70 } 71 buf.WriteString(color.Color("[reset]\n")) 72 73 switch change.Action { 74 case plans.Create: 75 buf.WriteString(color.Color("[green] +[reset] ")) 76 case plans.Read: 77 buf.WriteString(color.Color("[cyan] <=[reset] ")) 78 case plans.Update: 79 buf.WriteString(color.Color("[yellow] ~[reset] ")) 80 case plans.DeleteThenCreate: 81 buf.WriteString(color.Color("[red]-[reset]/[green]+[reset] ")) 82 case plans.CreateThenDelete: 83 buf.WriteString(color.Color("[green]+[reset]/[red]-[reset] ")) 84 case plans.Delete: 85 buf.WriteString(color.Color("[red] -[reset] ")) 86 default: 87 buf.WriteString(color.Color("??? ")) 88 } 89 90 switch addr.Resource.Resource.Mode { 91 case addrs.ManagedResourceMode: 92 buf.WriteString(fmt.Sprintf( 93 "resource %q %q", 94 addr.Resource.Resource.Type, 95 addr.Resource.Resource.Name, 96 )) 97 case addrs.DataResourceMode: 98 buf.WriteString(fmt.Sprintf( 99 "data %q %q ", 100 addr.Resource.Resource.Type, 101 addr.Resource.Resource.Name, 102 )) 103 default: 104 // should never happen, since the above is exhaustive 105 buf.WriteString(addr.String()) 106 } 107 108 buf.WriteString(" {") 109 110 p := blockBodyDiffPrinter{ 111 buf: &buf, 112 color: color, 113 action: change.Action, 114 requiredReplace: change.RequiredReplace, 115 } 116 117 // Most commonly-used resources have nested blocks that result in us 118 // going at least three traversals deep while we recurse here, so we'll 119 // start with that much capacity and then grow as needed for deeper 120 // structures. 121 path := make(cty.Path, 0, 3) 122 123 changeV, err := change.Decode(schema.ImpliedType()) 124 if err != nil { 125 // Should never happen in here, since we've already been through 126 // loads of layers of encode/decode of the planned changes before now. 127 panic(fmt.Sprintf("failed to decode plan for %s while rendering diff: %s", addr, err)) 128 } 129 130 // We currently have an opt-out that permits the legacy SDK to return values 131 // that defy our usual conventions around handling of nesting blocks. To 132 // avoid the rendering code from needing to handle all of these, we'll 133 // normalize first. 134 // (Ideally we'd do this as part of the SDK opt-out implementation in core, 135 // but we've added it here for now to reduce risk of unexpected impacts 136 // on other code in core.) 137 changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, schema) 138 changeV.Change.After = objchange.NormalizeObjectFromLegacySDK(changeV.Change.After, schema) 139 140 bodyWritten := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path) 141 if bodyWritten { 142 buf.WriteString("\n") 143 buf.WriteString(strings.Repeat(" ", 4)) 144 } 145 buf.WriteString("}\n") 146 147 return buf.String() 148 } 149 150 type blockBodyDiffPrinter struct { 151 buf *bytes.Buffer 152 color *colorstring.Colorize 153 action plans.Action 154 requiredReplace cty.PathSet 155 } 156 157 const forcesNewResourceCaption = " [red]# forces replacement[reset]" 158 159 // writeBlockBodyDiff writes attribute or block differences 160 // and returns true if any differences were found and written 161 func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, old, new cty.Value, indent int, path cty.Path) bool { 162 path = ctyEnsurePathCapacity(path, 1) 163 164 bodyWritten := false 165 blankBeforeBlocks := false 166 { 167 attrNames := make([]string, 0, len(schema.Attributes)) 168 attrNameLen := 0 169 for name := range schema.Attributes { 170 oldVal := ctyGetAttrMaybeNull(old, name) 171 newVal := ctyGetAttrMaybeNull(new, name) 172 if oldVal.IsNull() && newVal.IsNull() { 173 // Skip attributes where both old and new values are null 174 // (we do this early here so that we'll do our value alignment 175 // based on the longest attribute name that has a change, rather 176 // than the longest attribute name in the full set.) 177 continue 178 } 179 180 attrNames = append(attrNames, name) 181 if len(name) > attrNameLen { 182 attrNameLen = len(name) 183 } 184 } 185 sort.Strings(attrNames) 186 if len(attrNames) > 0 { 187 blankBeforeBlocks = true 188 } 189 190 for _, name := range attrNames { 191 attrS := schema.Attributes[name] 192 oldVal := ctyGetAttrMaybeNull(old, name) 193 newVal := ctyGetAttrMaybeNull(new, name) 194 195 bodyWritten = true 196 p.writeAttrDiff(name, attrS, oldVal, newVal, attrNameLen, indent, path) 197 } 198 } 199 200 { 201 blockTypeNames := make([]string, 0, len(schema.BlockTypes)) 202 for name := range schema.BlockTypes { 203 blockTypeNames = append(blockTypeNames, name) 204 } 205 sort.Strings(blockTypeNames) 206 207 for _, name := range blockTypeNames { 208 blockS := schema.BlockTypes[name] 209 oldVal := ctyGetAttrMaybeNull(old, name) 210 newVal := ctyGetAttrMaybeNull(new, name) 211 212 bodyWritten = true 213 p.writeNestedBlockDiffs(name, blockS, oldVal, newVal, blankBeforeBlocks, indent, path) 214 215 // Always include a blank for any subsequent block types. 216 blankBeforeBlocks = true 217 } 218 } 219 220 return bodyWritten 221 } 222 223 func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) { 224 path = append(path, cty.GetAttrStep{Name: name}) 225 p.buf.WriteString("\n") 226 p.buf.WriteString(strings.Repeat(" ", indent)) 227 showJustNew := false 228 var action plans.Action 229 switch { 230 case old.IsNull(): 231 action = plans.Create 232 showJustNew = true 233 case new.IsNull(): 234 action = plans.Delete 235 case ctyEqualWithUnknown(old, new): 236 action = plans.NoOp 237 showJustNew = true 238 default: 239 action = plans.Update 240 } 241 242 p.writeActionSymbol(action) 243 244 p.buf.WriteString(p.color.Color("[bold]")) 245 p.buf.WriteString(name) 246 p.buf.WriteString(p.color.Color("[reset]")) 247 p.buf.WriteString(strings.Repeat(" ", nameLen-len(name))) 248 p.buf.WriteString(" = ") 249 250 if attrS.Sensitive { 251 p.buf.WriteString("(sensitive value)") 252 } else { 253 switch { 254 case showJustNew: 255 p.writeValue(new, action, indent+2) 256 if p.pathForcesNewResource(path) { 257 p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) 258 } 259 default: 260 // We show new even if it is null to emphasize the fact 261 // that it is being unset, since otherwise it is easy to 262 // misunderstand that the value is still set to the old value. 263 p.writeValueDiff(old, new, indent+2, path) 264 } 265 } 266 } 267 268 func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *configschema.NestedBlock, old, new cty.Value, blankBefore bool, indent int, path cty.Path) { 269 path = append(path, cty.GetAttrStep{Name: name}) 270 if old.IsNull() && new.IsNull() { 271 // Nothing to do if both old and new is null 272 return 273 } 274 275 // Where old/new are collections representing a nesting mode other than 276 // NestingSingle, we assume the collection value can never be unknown 277 // since we always produce the container for the nested objects, even if 278 // the objects within are computed. 279 280 switch blockS.Nesting { 281 case configschema.NestingSingle, configschema.NestingGroup: 282 var action plans.Action 283 eqV := new.Equals(old) 284 switch { 285 case old.IsNull(): 286 action = plans.Create 287 case new.IsNull(): 288 action = plans.Delete 289 case !new.IsWhollyKnown() || !old.IsWhollyKnown(): 290 // "old" should actually always be known due to our contract 291 // that old values must never be unknown, but we'll allow it 292 // anyway to be robust. 293 action = plans.Update 294 case !eqV.IsKnown() || !eqV.True(): 295 action = plans.Update 296 } 297 298 if blankBefore { 299 p.buf.WriteRune('\n') 300 } 301 p.writeNestedBlockDiff(name, nil, &blockS.Block, action, old, new, indent, path) 302 case configschema.NestingList: 303 // For the sake of handling nested blocks, we'll treat a null list 304 // the same as an empty list since the config language doesn't 305 // distinguish these anyway. 306 old = ctyNullBlockListAsEmpty(old) 307 new = ctyNullBlockListAsEmpty(new) 308 309 oldItems := ctyCollectionValues(old) 310 newItems := ctyCollectionValues(new) 311 312 // Here we intentionally preserve the index-based correspondance 313 // between old and new, rather than trying to detect insertions 314 // and removals in the list, because this more accurately reflects 315 // how Terraform Core and providers will understand the change, 316 // particularly when the nested block contains computed attributes 317 // that will themselves maintain correspondance by index. 318 319 // commonLen is number of elements that exist in both lists, which 320 // will be presented as updates (~). Any additional items in one 321 // of the lists will be presented as either creates (+) or deletes (-) 322 // depending on which list they belong to. 323 var commonLen int 324 switch { 325 case len(oldItems) < len(newItems): 326 commonLen = len(oldItems) 327 default: 328 commonLen = len(newItems) 329 } 330 331 if blankBefore && (len(oldItems) > 0 || len(newItems) > 0) { 332 p.buf.WriteRune('\n') 333 } 334 335 for i := 0; i < commonLen; i++ { 336 path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) 337 oldItem := oldItems[i] 338 newItem := newItems[i] 339 action := plans.Update 340 if oldItem.RawEquals(newItem) { 341 action = plans.NoOp 342 } 343 p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldItem, newItem, indent, path) 344 } 345 for i := commonLen; i < len(oldItems); i++ { 346 path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) 347 oldItem := oldItems[i] 348 newItem := cty.NullVal(oldItem.Type()) 349 p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Delete, oldItem, newItem, indent, path) 350 } 351 for i := commonLen; i < len(newItems); i++ { 352 path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) 353 newItem := newItems[i] 354 oldItem := cty.NullVal(newItem.Type()) 355 p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Create, oldItem, newItem, indent, path) 356 } 357 case configschema.NestingSet: 358 // For the sake of handling nested blocks, we'll treat a null set 359 // the same as an empty set since the config language doesn't 360 // distinguish these anyway. 361 old = ctyNullBlockSetAsEmpty(old) 362 new = ctyNullBlockSetAsEmpty(new) 363 364 oldItems := ctyCollectionValues(old) 365 newItems := ctyCollectionValues(new) 366 367 if (len(oldItems) + len(newItems)) == 0 { 368 // Nothing to do if both sets are empty 369 return 370 } 371 372 allItems := make([]cty.Value, 0, len(oldItems)+len(newItems)) 373 allItems = append(allItems, oldItems...) 374 allItems = append(allItems, newItems...) 375 all := cty.SetVal(allItems) 376 377 if blankBefore { 378 p.buf.WriteRune('\n') 379 } 380 381 for it := all.ElementIterator(); it.Next(); { 382 _, val := it.Element() 383 var action plans.Action 384 var oldValue, newValue cty.Value 385 switch { 386 case !val.IsKnown(): 387 action = plans.Update 388 newValue = val 389 case !old.HasElement(val).True(): 390 action = plans.Create 391 oldValue = cty.NullVal(val.Type()) 392 newValue = val 393 case !new.HasElement(val).True(): 394 action = plans.Delete 395 oldValue = val 396 newValue = cty.NullVal(val.Type()) 397 default: 398 action = plans.NoOp 399 oldValue = val 400 newValue = val 401 } 402 path := append(path, cty.IndexStep{Key: val}) 403 p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldValue, newValue, indent, path) 404 } 405 406 case configschema.NestingMap: 407 // For the sake of handling nested blocks, we'll treat a null map 408 // the same as an empty map since the config language doesn't 409 // distinguish these anyway. 410 old = ctyNullBlockMapAsEmpty(old) 411 new = ctyNullBlockMapAsEmpty(new) 412 413 oldItems := old.AsValueMap() 414 newItems := new.AsValueMap() 415 if (len(oldItems) + len(newItems)) == 0 { 416 // Nothing to do if both maps are empty 417 return 418 } 419 420 allKeys := make(map[string]bool) 421 for k := range oldItems { 422 allKeys[k] = true 423 } 424 for k := range newItems { 425 allKeys[k] = true 426 } 427 allKeysOrder := make([]string, 0, len(allKeys)) 428 for k := range allKeys { 429 allKeysOrder = append(allKeysOrder, k) 430 } 431 sort.Strings(allKeysOrder) 432 433 if blankBefore { 434 p.buf.WriteRune('\n') 435 } 436 437 for _, k := range allKeysOrder { 438 var action plans.Action 439 oldValue := oldItems[k] 440 newValue := newItems[k] 441 switch { 442 case oldValue == cty.NilVal: 443 oldValue = cty.NullVal(newValue.Type()) 444 action = plans.Create 445 case newValue == cty.NilVal: 446 newValue = cty.NullVal(oldValue.Type()) 447 action = plans.Delete 448 case !newValue.RawEquals(oldValue): 449 action = plans.Update 450 default: 451 action = plans.NoOp 452 } 453 454 path := append(path, cty.IndexStep{Key: cty.StringVal(k)}) 455 p.writeNestedBlockDiff(name, &k, &blockS.Block, action, oldValue, newValue, indent, path) 456 } 457 } 458 } 459 460 func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string, blockS *configschema.Block, action plans.Action, old, new cty.Value, indent int, path cty.Path) { 461 p.buf.WriteString("\n") 462 p.buf.WriteString(strings.Repeat(" ", indent)) 463 p.writeActionSymbol(action) 464 465 if label != nil { 466 fmt.Fprintf(p.buf, "%s %q {", name, *label) 467 } else { 468 fmt.Fprintf(p.buf, "%s {", name) 469 } 470 471 if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) { 472 p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) 473 } 474 475 bodyWritten := p.writeBlockBodyDiff(blockS, old, new, indent+4, path) 476 if bodyWritten { 477 p.buf.WriteString("\n") 478 p.buf.WriteString(strings.Repeat(" ", indent+2)) 479 } 480 p.buf.WriteString("}") 481 } 482 483 func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) { 484 if !val.IsKnown() { 485 p.buf.WriteString("(known after apply)") 486 return 487 } 488 if val.IsNull() { 489 p.buf.WriteString(p.color.Color("[dark_gray]null[reset]")) 490 return 491 } 492 493 ty := val.Type() 494 495 switch { 496 case ty.IsPrimitiveType(): 497 switch ty { 498 case cty.String: 499 { 500 // Special behavior for JSON strings containing array or object 501 src := []byte(val.AsString()) 502 ty, err := ctyjson.ImpliedType(src) 503 // check for the special case of "null", which decodes to nil, 504 // and just allow it to be printed out directly 505 if err == nil && !ty.IsPrimitiveType() && val.AsString() != "null" { 506 jv, err := ctyjson.Unmarshal(src, ty) 507 if err == nil { 508 p.buf.WriteString("jsonencode(") 509 if jv.LengthInt() == 0 { 510 p.writeValue(jv, action, 0) 511 } else { 512 p.buf.WriteByte('\n') 513 p.buf.WriteString(strings.Repeat(" ", indent+4)) 514 p.writeValue(jv, action, indent+4) 515 p.buf.WriteByte('\n') 516 p.buf.WriteString(strings.Repeat(" ", indent)) 517 } 518 p.buf.WriteByte(')') 519 break // don't *also* do the normal behavior below 520 } 521 } 522 } 523 fmt.Fprintf(p.buf, "%q", val.AsString()) 524 case cty.Bool: 525 if val.True() { 526 p.buf.WriteString("true") 527 } else { 528 p.buf.WriteString("false") 529 } 530 case cty.Number: 531 bf := val.AsBigFloat() 532 p.buf.WriteString(bf.Text('f', -1)) 533 default: 534 // should never happen, since the above is exhaustive 535 fmt.Fprintf(p.buf, "%#v", val) 536 } 537 case ty.IsListType() || ty.IsSetType() || ty.IsTupleType(): 538 p.buf.WriteString("[") 539 540 it := val.ElementIterator() 541 for it.Next() { 542 _, val := it.Element() 543 544 p.buf.WriteString("\n") 545 p.buf.WriteString(strings.Repeat(" ", indent+2)) 546 p.writeActionSymbol(action) 547 p.writeValue(val, action, indent+4) 548 p.buf.WriteString(",") 549 } 550 551 if val.LengthInt() > 0 { 552 p.buf.WriteString("\n") 553 p.buf.WriteString(strings.Repeat(" ", indent)) 554 } 555 p.buf.WriteString("]") 556 case ty.IsMapType(): 557 p.buf.WriteString("{") 558 559 keyLen := 0 560 for it := val.ElementIterator(); it.Next(); { 561 key, _ := it.Element() 562 if keyStr := key.AsString(); len(keyStr) > keyLen { 563 keyLen = len(keyStr) 564 } 565 } 566 567 for it := val.ElementIterator(); it.Next(); { 568 key, val := it.Element() 569 570 p.buf.WriteString("\n") 571 p.buf.WriteString(strings.Repeat(" ", indent+2)) 572 p.writeActionSymbol(action) 573 p.writeValue(key, action, indent+4) 574 p.buf.WriteString(strings.Repeat(" ", keyLen-len(key.AsString()))) 575 p.buf.WriteString(" = ") 576 p.writeValue(val, action, indent+4) 577 } 578 579 if val.LengthInt() > 0 { 580 p.buf.WriteString("\n") 581 p.buf.WriteString(strings.Repeat(" ", indent)) 582 } 583 p.buf.WriteString("}") 584 case ty.IsObjectType(): 585 p.buf.WriteString("{") 586 587 atys := ty.AttributeTypes() 588 attrNames := make([]string, 0, len(atys)) 589 nameLen := 0 590 for attrName := range atys { 591 attrNames = append(attrNames, attrName) 592 if len(attrName) > nameLen { 593 nameLen = len(attrName) 594 } 595 } 596 sort.Strings(attrNames) 597 598 for _, attrName := range attrNames { 599 val := val.GetAttr(attrName) 600 601 p.buf.WriteString("\n") 602 p.buf.WriteString(strings.Repeat(" ", indent+2)) 603 p.writeActionSymbol(action) 604 p.buf.WriteString(attrName) 605 p.buf.WriteString(strings.Repeat(" ", nameLen-len(attrName))) 606 p.buf.WriteString(" = ") 607 p.writeValue(val, action, indent+4) 608 } 609 610 if len(attrNames) > 0 { 611 p.buf.WriteString("\n") 612 p.buf.WriteString(strings.Repeat(" ", indent)) 613 } 614 p.buf.WriteString("}") 615 } 616 } 617 618 func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, path cty.Path) { 619 ty := old.Type() 620 typesEqual := ctyTypesEqual(ty, new.Type()) 621 622 // We have some specialized diff implementations for certain complex 623 // values where it's useful to see a visualization of the diff of 624 // the nested elements rather than just showing the entire old and 625 // new values verbatim. 626 // However, these specialized implementations can apply only if both 627 // values are known and non-null. 628 if old.IsKnown() && new.IsKnown() && !old.IsNull() && !new.IsNull() && typesEqual { 629 switch { 630 case ty == cty.String: 631 // We have special behavior for both multi-line strings in general 632 // and for strings that can parse as JSON. For the JSON handling 633 // to apply, both old and new must be valid JSON. 634 // For single-line strings that don't parse as JSON we just fall 635 // out of this switch block and do the default old -> new rendering. 636 oldS := old.AsString() 637 newS := new.AsString() 638 639 { 640 // Special behavior for JSON strings containing object or 641 // list values. 642 oldBytes := []byte(oldS) 643 newBytes := []byte(newS) 644 oldType, oldErr := ctyjson.ImpliedType(oldBytes) 645 newType, newErr := ctyjson.ImpliedType(newBytes) 646 if oldErr == nil && newErr == nil && !(oldType.IsPrimitiveType() && newType.IsPrimitiveType()) { 647 oldJV, oldErr := ctyjson.Unmarshal(oldBytes, oldType) 648 newJV, newErr := ctyjson.Unmarshal(newBytes, newType) 649 if oldErr == nil && newErr == nil { 650 if !oldJV.RawEquals(newJV) { // two JSON values may differ only in insignificant whitespace 651 p.buf.WriteString("jsonencode(") 652 p.buf.WriteByte('\n') 653 p.buf.WriteString(strings.Repeat(" ", indent+2)) 654 p.writeActionSymbol(plans.Update) 655 p.writeValueDiff(oldJV, newJV, indent+4, path) 656 p.buf.WriteByte('\n') 657 p.buf.WriteString(strings.Repeat(" ", indent)) 658 p.buf.WriteByte(')') 659 } else { 660 // if they differ only in insigificant whitespace 661 // then we'll note that but still expand out the 662 // effective value. 663 if p.pathForcesNewResource(path) { 664 p.buf.WriteString(p.color.Color("jsonencode( [red]# whitespace changes force replacement[reset]")) 665 } else { 666 p.buf.WriteString(p.color.Color("jsonencode( [dim]# whitespace changes[reset]")) 667 } 668 p.buf.WriteByte('\n') 669 p.buf.WriteString(strings.Repeat(" ", indent+4)) 670 p.writeValue(oldJV, plans.NoOp, indent+4) 671 p.buf.WriteByte('\n') 672 p.buf.WriteString(strings.Repeat(" ", indent)) 673 p.buf.WriteByte(')') 674 } 675 return 676 } 677 } 678 } 679 680 if strings.Index(oldS, "\n") < 0 && strings.Index(newS, "\n") < 0 { 681 break 682 } 683 684 p.buf.WriteString("<<~EOT") 685 if p.pathForcesNewResource(path) { 686 p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) 687 } 688 p.buf.WriteString("\n") 689 690 var oldLines, newLines []cty.Value 691 { 692 r := strings.NewReader(oldS) 693 sc := bufio.NewScanner(r) 694 for sc.Scan() { 695 oldLines = append(oldLines, cty.StringVal(sc.Text())) 696 } 697 } 698 { 699 r := strings.NewReader(newS) 700 sc := bufio.NewScanner(r) 701 for sc.Scan() { 702 newLines = append(newLines, cty.StringVal(sc.Text())) 703 } 704 } 705 706 diffLines := ctySequenceDiff(oldLines, newLines) 707 for _, diffLine := range diffLines { 708 p.buf.WriteString(strings.Repeat(" ", indent+2)) 709 p.writeActionSymbol(diffLine.Action) 710 711 switch diffLine.Action { 712 case plans.NoOp, plans.Delete: 713 p.buf.WriteString(diffLine.Before.AsString()) 714 case plans.Create: 715 p.buf.WriteString(diffLine.After.AsString()) 716 default: 717 // Should never happen since the above covers all 718 // actions that ctySequenceDiff can return for strings 719 p.buf.WriteString(diffLine.After.AsString()) 720 721 } 722 p.buf.WriteString("\n") 723 } 724 725 p.buf.WriteString(strings.Repeat(" ", indent)) // +4 here because there's no symbol 726 p.buf.WriteString("EOT") 727 728 return 729 730 case ty.IsSetType(): 731 p.buf.WriteString("[") 732 if p.pathForcesNewResource(path) { 733 p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) 734 } 735 p.buf.WriteString("\n") 736 737 var addedVals, removedVals, allVals []cty.Value 738 for it := old.ElementIterator(); it.Next(); { 739 _, val := it.Element() 740 allVals = append(allVals, val) 741 if new.HasElement(val).False() { 742 removedVals = append(removedVals, val) 743 } 744 } 745 for it := new.ElementIterator(); it.Next(); { 746 _, val := it.Element() 747 allVals = append(allVals, val) 748 if val.IsKnown() && old.HasElement(val).False() { 749 addedVals = append(addedVals, val) 750 } 751 } 752 753 var all, added, removed cty.Value 754 if len(allVals) > 0 { 755 all = cty.SetVal(allVals) 756 } else { 757 all = cty.SetValEmpty(ty.ElementType()) 758 } 759 if len(addedVals) > 0 { 760 added = cty.SetVal(addedVals) 761 } else { 762 added = cty.SetValEmpty(ty.ElementType()) 763 } 764 if len(removedVals) > 0 { 765 removed = cty.SetVal(removedVals) 766 } else { 767 removed = cty.SetValEmpty(ty.ElementType()) 768 } 769 770 for it := all.ElementIterator(); it.Next(); { 771 _, val := it.Element() 772 773 p.buf.WriteString(strings.Repeat(" ", indent+2)) 774 775 var action plans.Action 776 switch { 777 case !val.IsKnown(): 778 action = plans.Update 779 case added.HasElement(val).True(): 780 action = plans.Create 781 case removed.HasElement(val).True(): 782 action = plans.Delete 783 default: 784 action = plans.NoOp 785 } 786 787 p.writeActionSymbol(action) 788 p.writeValue(val, action, indent+4) 789 p.buf.WriteString(",\n") 790 } 791 792 p.buf.WriteString(strings.Repeat(" ", indent)) 793 p.buf.WriteString("]") 794 return 795 case ty.IsListType() || ty.IsTupleType(): 796 p.buf.WriteString("[") 797 if p.pathForcesNewResource(path) { 798 p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) 799 } 800 p.buf.WriteString("\n") 801 802 elemDiffs := ctySequenceDiff(old.AsValueSlice(), new.AsValueSlice()) 803 for _, elemDiff := range elemDiffs { 804 p.buf.WriteString(strings.Repeat(" ", indent+2)) 805 p.writeActionSymbol(elemDiff.Action) 806 switch elemDiff.Action { 807 case plans.NoOp, plans.Delete: 808 p.writeValue(elemDiff.Before, elemDiff.Action, indent+4) 809 case plans.Update: 810 p.writeValueDiff(elemDiff.Before, elemDiff.After, indent+4, path) 811 case plans.Create: 812 p.writeValue(elemDiff.After, elemDiff.Action, indent+4) 813 default: 814 // Should never happen since the above covers all 815 // actions that ctySequenceDiff can return. 816 p.writeValue(elemDiff.After, elemDiff.Action, indent+4) 817 } 818 819 p.buf.WriteString(",\n") 820 } 821 822 p.buf.WriteString(strings.Repeat(" ", indent)) 823 p.buf.WriteString("]") 824 return 825 826 case ty.IsMapType(): 827 p.buf.WriteString("{") 828 if p.pathForcesNewResource(path) { 829 p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) 830 } 831 p.buf.WriteString("\n") 832 833 var allKeys []string 834 keyLen := 0 835 for it := old.ElementIterator(); it.Next(); { 836 k, _ := it.Element() 837 keyStr := k.AsString() 838 allKeys = append(allKeys, keyStr) 839 if len(keyStr) > keyLen { 840 keyLen = len(keyStr) 841 } 842 } 843 for it := new.ElementIterator(); it.Next(); { 844 k, _ := it.Element() 845 keyStr := k.AsString() 846 allKeys = append(allKeys, keyStr) 847 if len(keyStr) > keyLen { 848 keyLen = len(keyStr) 849 } 850 } 851 852 sort.Strings(allKeys) 853 854 lastK := "" 855 for i, k := range allKeys { 856 if i > 0 && lastK == k { 857 continue // skip duplicates (list is sorted) 858 } 859 lastK = k 860 861 p.buf.WriteString(strings.Repeat(" ", indent+2)) 862 kV := cty.StringVal(k) 863 var action plans.Action 864 if old.HasIndex(kV).False() { 865 action = plans.Create 866 } else if new.HasIndex(kV).False() { 867 action = plans.Delete 868 } else if eqV := old.Index(kV).Equals(new.Index(kV)); eqV.IsKnown() && eqV.True() { 869 action = plans.NoOp 870 } else { 871 action = plans.Update 872 } 873 874 path := append(path, cty.IndexStep{Key: kV}) 875 876 p.writeActionSymbol(action) 877 p.writeValue(kV, action, indent+4) 878 p.buf.WriteString(strings.Repeat(" ", keyLen-len(k))) 879 p.buf.WriteString(" = ") 880 switch action { 881 case plans.Create, plans.NoOp: 882 v := new.Index(kV) 883 p.writeValue(v, action, indent+4) 884 case plans.Delete: 885 oldV := old.Index(kV) 886 newV := cty.NullVal(oldV.Type()) 887 p.writeValueDiff(oldV, newV, indent+4, path) 888 default: 889 oldV := old.Index(kV) 890 newV := new.Index(kV) 891 p.writeValueDiff(oldV, newV, indent+4, path) 892 } 893 894 p.buf.WriteByte('\n') 895 } 896 897 p.buf.WriteString(strings.Repeat(" ", indent)) 898 p.buf.WriteString("}") 899 return 900 case ty.IsObjectType(): 901 p.buf.WriteString("{") 902 p.buf.WriteString("\n") 903 904 forcesNewResource := p.pathForcesNewResource(path) 905 906 var allKeys []string 907 keyLen := 0 908 for it := old.ElementIterator(); it.Next(); { 909 k, _ := it.Element() 910 keyStr := k.AsString() 911 allKeys = append(allKeys, keyStr) 912 if len(keyStr) > keyLen { 913 keyLen = len(keyStr) 914 } 915 } 916 for it := new.ElementIterator(); it.Next(); { 917 k, _ := it.Element() 918 keyStr := k.AsString() 919 allKeys = append(allKeys, keyStr) 920 if len(keyStr) > keyLen { 921 keyLen = len(keyStr) 922 } 923 } 924 925 sort.Strings(allKeys) 926 927 lastK := "" 928 for i, k := range allKeys { 929 if i > 0 && lastK == k { 930 continue // skip duplicates (list is sorted) 931 } 932 lastK = k 933 934 p.buf.WriteString(strings.Repeat(" ", indent+2)) 935 kV := k 936 var action plans.Action 937 if !old.Type().HasAttribute(kV) { 938 action = plans.Create 939 } else if !new.Type().HasAttribute(kV) { 940 action = plans.Delete 941 } else if eqV := old.GetAttr(kV).Equals(new.GetAttr(kV)); eqV.IsKnown() && eqV.True() { 942 action = plans.NoOp 943 } else { 944 action = plans.Update 945 } 946 947 path := append(path, cty.GetAttrStep{Name: kV}) 948 949 p.writeActionSymbol(action) 950 p.buf.WriteString(k) 951 p.buf.WriteString(strings.Repeat(" ", keyLen-len(k))) 952 p.buf.WriteString(" = ") 953 954 switch action { 955 case plans.Create, plans.NoOp: 956 v := new.GetAttr(kV) 957 p.writeValue(v, action, indent+4) 958 case plans.Delete: 959 oldV := old.GetAttr(kV) 960 newV := cty.NullVal(oldV.Type()) 961 p.writeValueDiff(oldV, newV, indent+4, path) 962 default: 963 oldV := old.GetAttr(kV) 964 newV := new.GetAttr(kV) 965 p.writeValueDiff(oldV, newV, indent+4, path) 966 } 967 968 p.buf.WriteString("\n") 969 } 970 971 p.buf.WriteString(strings.Repeat(" ", indent)) 972 p.buf.WriteString("}") 973 974 if forcesNewResource { 975 p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) 976 } 977 return 978 } 979 } 980 981 // In all other cases, we just show the new and old values as-is 982 p.writeValue(old, plans.Delete, indent) 983 if new.IsNull() { 984 p.buf.WriteString(p.color.Color(" [dark_gray]->[reset] ")) 985 } else { 986 p.buf.WriteString(p.color.Color(" [yellow]->[reset] ")) 987 } 988 989 p.writeValue(new, plans.Create, indent) 990 if p.pathForcesNewResource(path) { 991 p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) 992 } 993 } 994 995 // writeActionSymbol writes a symbol to represent the given action, followed 996 // by a space. 997 // 998 // It only supports the actions that can be represented with a single character: 999 // Create, Delete, Update and NoAction. 1000 func (p *blockBodyDiffPrinter) writeActionSymbol(action plans.Action) { 1001 switch action { 1002 case plans.Create: 1003 p.buf.WriteString(p.color.Color("[green]+[reset] ")) 1004 case plans.Delete: 1005 p.buf.WriteString(p.color.Color("[red]-[reset] ")) 1006 case plans.Update: 1007 p.buf.WriteString(p.color.Color("[yellow]~[reset] ")) 1008 case plans.NoOp: 1009 p.buf.WriteString(" ") 1010 default: 1011 // Should never happen 1012 p.buf.WriteString(p.color.Color("? ")) 1013 } 1014 } 1015 1016 func (p *blockBodyDiffPrinter) pathForcesNewResource(path cty.Path) bool { 1017 if !p.action.IsReplace() { 1018 // "requiredReplace" only applies when the instance is being replaced 1019 return false 1020 } 1021 return p.requiredReplace.Has(path) 1022 } 1023 1024 func ctyEmptyString(value cty.Value) bool { 1025 if !value.IsNull() && value.IsKnown() { 1026 valueType := value.Type() 1027 if valueType == cty.String && value.AsString() == "" { 1028 return true 1029 } 1030 } 1031 return false 1032 } 1033 1034 func ctyGetAttrMaybeNull(val cty.Value, name string) cty.Value { 1035 attrType := val.Type().AttributeType(name) 1036 1037 if val.IsNull() { 1038 return cty.NullVal(attrType) 1039 } 1040 1041 // We treat "" as null here 1042 // as existing SDK doesn't support null yet. 1043 // This allows us to avoid spurious diffs 1044 // until we introduce null to the SDK. 1045 attrValue := val.GetAttr(name) 1046 if ctyEmptyString(attrValue) { 1047 return cty.NullVal(attrType) 1048 } 1049 1050 return attrValue 1051 } 1052 1053 func ctyCollectionValues(val cty.Value) []cty.Value { 1054 if !val.IsKnown() || val.IsNull() { 1055 return nil 1056 } 1057 1058 ret := make([]cty.Value, 0, val.LengthInt()) 1059 for it := val.ElementIterator(); it.Next(); { 1060 _, value := it.Element() 1061 ret = append(ret, value) 1062 } 1063 return ret 1064 } 1065 1066 // ctySequenceDiff returns differences between given sequences of cty.Value(s) 1067 // in the form of Create, Delete, or Update actions (for objects). 1068 func ctySequenceDiff(old, new []cty.Value) []*plans.Change { 1069 var ret []*plans.Change 1070 lcs := objchange.LongestCommonSubsequence(old, new) 1071 var oldI, newI, lcsI int 1072 for oldI < len(old) || newI < len(new) || lcsI < len(lcs) { 1073 for oldI < len(old) && (lcsI >= len(lcs) || !old[oldI].RawEquals(lcs[lcsI])) { 1074 isObjectDiff := old[oldI].Type().IsObjectType() && (newI >= len(new) || new[newI].Type().IsObjectType()) 1075 if isObjectDiff && newI < len(new) { 1076 ret = append(ret, &plans.Change{ 1077 Action: plans.Update, 1078 Before: old[oldI], 1079 After: new[newI], 1080 }) 1081 oldI++ 1082 newI++ // we also consume the next "new" in this case 1083 continue 1084 } 1085 1086 ret = append(ret, &plans.Change{ 1087 Action: plans.Delete, 1088 Before: old[oldI], 1089 After: cty.NullVal(old[oldI].Type()), 1090 }) 1091 oldI++ 1092 } 1093 for newI < len(new) && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI])) { 1094 ret = append(ret, &plans.Change{ 1095 Action: plans.Create, 1096 Before: cty.NullVal(new[newI].Type()), 1097 After: new[newI], 1098 }) 1099 newI++ 1100 } 1101 if lcsI < len(lcs) { 1102 ret = append(ret, &plans.Change{ 1103 Action: plans.NoOp, 1104 Before: lcs[lcsI], 1105 After: lcs[lcsI], 1106 }) 1107 1108 // All of our indexes advance together now, since the line 1109 // is common to all three sequences. 1110 lcsI++ 1111 oldI++ 1112 newI++ 1113 } 1114 } 1115 return ret 1116 } 1117 1118 func ctyEqualWithUnknown(old, new cty.Value) bool { 1119 if !old.IsWhollyKnown() || !new.IsWhollyKnown() { 1120 return false 1121 } 1122 return old.Equals(new).True() 1123 } 1124 1125 // ctyTypesEqual checks equality of two types more loosely 1126 // by avoiding checks of object/tuple elements 1127 // as we render differences on element-by-element basis anyway 1128 func ctyTypesEqual(oldT, newT cty.Type) bool { 1129 if oldT.IsObjectType() && newT.IsObjectType() { 1130 return true 1131 } 1132 if oldT.IsTupleType() && newT.IsTupleType() { 1133 return true 1134 } 1135 return oldT.Equals(newT) 1136 } 1137 1138 func ctyEnsurePathCapacity(path cty.Path, minExtra int) cty.Path { 1139 if cap(path)-len(path) >= minExtra { 1140 return path 1141 } 1142 newCap := cap(path) * 2 1143 if newCap < (len(path) + minExtra) { 1144 newCap = len(path) + minExtra 1145 } 1146 newPath := make(cty.Path, len(path), newCap) 1147 copy(newPath, path) 1148 return newPath 1149 } 1150 1151 // ctyNullBlockListAsEmpty either returns the given value verbatim if it is non-nil 1152 // or returns an empty value of a suitable type to serve as a placeholder for it. 1153 // 1154 // In particular, this function handles the special situation where a "list" is 1155 // actually represented as a tuple type where nested blocks contain 1156 // dynamically-typed values. 1157 func ctyNullBlockListAsEmpty(in cty.Value) cty.Value { 1158 if !in.IsNull() { 1159 return in 1160 } 1161 if ty := in.Type(); ty.IsListType() { 1162 return cty.ListValEmpty(ty.ElementType()) 1163 } 1164 return cty.EmptyTupleVal // must need a tuple, then 1165 } 1166 1167 // ctyNullBlockMapAsEmpty either returns the given value verbatim if it is non-nil 1168 // or returns an empty value of a suitable type to serve as a placeholder for it. 1169 // 1170 // In particular, this function handles the special situation where a "map" is 1171 // actually represented as an object type where nested blocks contain 1172 // dynamically-typed values. 1173 func ctyNullBlockMapAsEmpty(in cty.Value) cty.Value { 1174 if !in.IsNull() { 1175 return in 1176 } 1177 if ty := in.Type(); ty.IsMapType() { 1178 return cty.MapValEmpty(ty.ElementType()) 1179 } 1180 return cty.EmptyObjectVal // must need an object, then 1181 } 1182 1183 // ctyNullBlockSetAsEmpty either returns the given value verbatim if it is non-nil 1184 // or returns an empty value of a suitable type to serve as a placeholder for it. 1185 func ctyNullBlockSetAsEmpty(in cty.Value) cty.Value { 1186 if !in.IsNull() { 1187 return in 1188 } 1189 // Dynamically-typed attributes are not supported inside blocks backed by 1190 // sets, so our result here is always a set. 1191 return cty.SetValEmpty(in.Type().ElementType()) 1192 }