github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/states/statefile/version4.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package statefile 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io" 10 "sort" 11 12 version "github.com/hashicorp/go-version" 13 "github.com/zclconf/go-cty/cty" 14 ctyjson "github.com/zclconf/go-cty/cty/json" 15 16 "github.com/terramate-io/tf/addrs" 17 "github.com/terramate-io/tf/checks" 18 "github.com/terramate-io/tf/lang/marks" 19 "github.com/terramate-io/tf/states" 20 "github.com/terramate-io/tf/tfdiags" 21 ) 22 23 func readStateV4(src []byte) (*File, tfdiags.Diagnostics) { 24 var diags tfdiags.Diagnostics 25 sV4 := &stateV4{} 26 err := json.Unmarshal(src, sV4) 27 if err != nil { 28 diags = diags.Append(jsonUnmarshalDiags(err)) 29 return nil, diags 30 } 31 32 file, prepDiags := prepareStateV4(sV4) 33 diags = diags.Append(prepDiags) 34 return file, diags 35 } 36 37 func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) { 38 var diags tfdiags.Diagnostics 39 40 var tfVersion *version.Version 41 if sV4.TerraformVersion != "" { 42 var err error 43 tfVersion, err = version.NewVersion(sV4.TerraformVersion) 44 if err != nil { 45 diags = diags.Append(tfdiags.Sourceless( 46 tfdiags.Error, 47 "Invalid Terraform version string", 48 fmt.Sprintf("State file claims to have been written by Terraform version %q, which is not a valid version string.", sV4.TerraformVersion), 49 )) 50 } 51 } 52 53 file := &File{ 54 TerraformVersion: tfVersion, 55 Serial: sV4.Serial, 56 Lineage: sV4.Lineage, 57 } 58 59 state := states.NewState() 60 61 for _, rsV4 := range sV4.Resources { 62 rAddr := addrs.Resource{ 63 Type: rsV4.Type, 64 Name: rsV4.Name, 65 } 66 switch rsV4.Mode { 67 case "managed": 68 rAddr.Mode = addrs.ManagedResourceMode 69 case "data": 70 rAddr.Mode = addrs.DataResourceMode 71 default: 72 diags = diags.Append(tfdiags.Sourceless( 73 tfdiags.Error, 74 "Invalid resource mode in state", 75 fmt.Sprintf("State contains a resource with mode %q (%q %q) which is not supported.", rsV4.Mode, rAddr.Type, rAddr.Name), 76 )) 77 continue 78 } 79 80 moduleAddr := addrs.RootModuleInstance 81 if rsV4.Module != "" { 82 var addrDiags tfdiags.Diagnostics 83 moduleAddr, addrDiags = addrs.ParseModuleInstanceStr(rsV4.Module) 84 diags = diags.Append(addrDiags) 85 if addrDiags.HasErrors() { 86 continue 87 } 88 } 89 90 providerAddr, addrDiags := addrs.ParseAbsProviderConfigStr(rsV4.ProviderConfig) 91 diags.Append(addrDiags) 92 if addrDiags.HasErrors() { 93 // If ParseAbsProviderConfigStr returns an error, the state may have 94 // been written before Provider FQNs were introduced and the 95 // AbsProviderConfig string format will need normalization. If so, 96 // we treat it like a legacy provider (namespace "-") and let the 97 // provider installer handle detecting the FQN. 98 var legacyAddrDiags tfdiags.Diagnostics 99 providerAddr, legacyAddrDiags = addrs.ParseLegacyAbsProviderConfigStr(rsV4.ProviderConfig) 100 if legacyAddrDiags.HasErrors() { 101 continue 102 } 103 } 104 105 ms := state.EnsureModule(moduleAddr) 106 107 // Ensure the resource container object is present in the state. 108 ms.SetResourceProvider(rAddr, providerAddr) 109 110 for _, isV4 := range rsV4.Instances { 111 keyRaw := isV4.IndexKey 112 var key addrs.InstanceKey 113 switch tk := keyRaw.(type) { 114 case int: 115 key = addrs.IntKey(tk) 116 case float64: 117 // Since JSON only has one number type, reading from encoding/json 118 // gives us a float64 here even if the number is whole. 119 // float64 has a smaller integer range than int, but in practice 120 // we rarely have more than a few tens of instances and so 121 // it's unlikely that we'll exhaust the 52 bits in a float64. 122 key = addrs.IntKey(int(tk)) 123 case string: 124 key = addrs.StringKey(tk) 125 default: 126 if keyRaw != nil { 127 diags = diags.Append(tfdiags.Sourceless( 128 tfdiags.Error, 129 "Invalid resource instance metadata in state", 130 fmt.Sprintf("Resource %s has an instance with the invalid instance key %#v.", rAddr.Absolute(moduleAddr), keyRaw), 131 )) 132 continue 133 } 134 key = addrs.NoKey 135 } 136 137 instAddr := rAddr.Instance(key) 138 139 obj := &states.ResourceInstanceObjectSrc{ 140 SchemaVersion: isV4.SchemaVersion, 141 CreateBeforeDestroy: isV4.CreateBeforeDestroy, 142 } 143 144 { 145 // Instance attributes 146 switch { 147 case isV4.AttributesRaw != nil: 148 obj.AttrsJSON = isV4.AttributesRaw 149 case isV4.AttributesFlat != nil: 150 obj.AttrsFlat = isV4.AttributesFlat 151 default: 152 // This is odd, but we'll accept it and just treat the 153 // object has being empty. In practice this should arise 154 // only from the contrived sort of state objects we tend 155 // to hand-write inline in tests. 156 obj.AttrsJSON = []byte{'{', '}'} 157 } 158 } 159 160 // Sensitive paths 161 if isV4.AttributeSensitivePaths != nil { 162 paths, pathsDiags := unmarshalPaths([]byte(isV4.AttributeSensitivePaths)) 163 diags = diags.Append(pathsDiags) 164 if pathsDiags.HasErrors() { 165 continue 166 } 167 168 var pvm []cty.PathValueMarks 169 for _, path := range paths { 170 pvm = append(pvm, cty.PathValueMarks{ 171 Path: path, 172 Marks: cty.NewValueMarks(marks.Sensitive), 173 }) 174 } 175 obj.AttrSensitivePaths = pvm 176 } 177 178 { 179 // Status 180 raw := isV4.Status 181 switch raw { 182 case "": 183 obj.Status = states.ObjectReady 184 case "tainted": 185 obj.Status = states.ObjectTainted 186 default: 187 diags = diags.Append(tfdiags.Sourceless( 188 tfdiags.Error, 189 "Invalid resource instance metadata in state", 190 fmt.Sprintf("Instance %s has invalid status %q.", instAddr.Absolute(moduleAddr), raw), 191 )) 192 continue 193 } 194 } 195 196 if raw := isV4.PrivateRaw; len(raw) > 0 { 197 obj.Private = raw 198 } 199 200 { 201 depsRaw := isV4.Dependencies 202 deps := make([]addrs.ConfigResource, 0, len(depsRaw)) 203 for _, depRaw := range depsRaw { 204 addr, addrDiags := addrs.ParseAbsResourceStr(depRaw) 205 diags = diags.Append(addrDiags) 206 if addrDiags.HasErrors() { 207 continue 208 } 209 deps = append(deps, addr.Config()) 210 } 211 obj.Dependencies = deps 212 } 213 214 switch { 215 case isV4.Deposed != "": 216 dk := states.DeposedKey(isV4.Deposed) 217 if len(dk) != 8 { 218 diags = diags.Append(tfdiags.Sourceless( 219 tfdiags.Error, 220 "Invalid resource instance metadata in state", 221 fmt.Sprintf("Instance %s has an object with deposed key %q, which is not correctly formatted.", instAddr.Absolute(moduleAddr), isV4.Deposed), 222 )) 223 continue 224 } 225 is := ms.ResourceInstance(instAddr) 226 if is.HasDeposed(dk) { 227 diags = diags.Append(tfdiags.Sourceless( 228 tfdiags.Error, 229 "Duplicate resource instance in state", 230 fmt.Sprintf("Instance %s deposed object %q appears multiple times in the state file.", instAddr.Absolute(moduleAddr), dk), 231 )) 232 continue 233 } 234 235 ms.SetResourceInstanceDeposed(instAddr, dk, obj, providerAddr) 236 default: 237 is := ms.ResourceInstance(instAddr) 238 if is.HasCurrent() { 239 diags = diags.Append(tfdiags.Sourceless( 240 tfdiags.Error, 241 "Duplicate resource instance in state", 242 fmt.Sprintf("Instance %s appears multiple times in the state file.", instAddr.Absolute(moduleAddr)), 243 )) 244 continue 245 } 246 247 ms.SetResourceInstanceCurrent(instAddr, obj, providerAddr) 248 } 249 } 250 251 // We repeat this after creating the instances because 252 // SetResourceInstanceCurrent automatically resets this metadata based 253 // on the incoming objects. That behavior is useful when we're making 254 // piecemeal updates to the state during an apply, but when we're 255 // reading the state file we want to reflect its contents exactly. 256 ms.SetResourceProvider(rAddr, providerAddr) 257 } 258 259 // The root module is special in that we persist its attributes and thus 260 // need to reload them now. (For descendent modules we just re-calculate 261 // them based on the latest configuration on each run.) 262 { 263 rootModule := state.RootModule() 264 for name, fos := range sV4.RootOutputs { 265 os := &states.OutputValue{ 266 Addr: addrs.AbsOutputValue{ 267 OutputValue: addrs.OutputValue{ 268 Name: name, 269 }, 270 }, 271 } 272 os.Sensitive = fos.Sensitive 273 274 ty, err := ctyjson.UnmarshalType([]byte(fos.ValueTypeRaw)) 275 if err != nil { 276 diags = diags.Append(tfdiags.Sourceless( 277 tfdiags.Error, 278 "Invalid output value type in state", 279 fmt.Sprintf("The state file has an invalid type specification for output %q: %s.", name, err), 280 )) 281 continue 282 } 283 284 val, err := ctyjson.Unmarshal([]byte(fos.ValueRaw), ty) 285 if err != nil { 286 diags = diags.Append(tfdiags.Sourceless( 287 tfdiags.Error, 288 "Invalid output value saved in state", 289 fmt.Sprintf("The state file has an invalid value for output %q: %s.", name, err), 290 )) 291 continue 292 } 293 294 os.Value = val 295 rootModule.OutputValues[name] = os 296 } 297 } 298 299 // Saved check results from the previous run, if any. 300 // We differentiate absense from an empty array here so that we can 301 // recognize if the previous run was with a version of Terraform that 302 // didn't support checks yet, or if there just weren't any checkable 303 // objects to record, in case that's important for certain messaging. 304 if sV4.CheckResults != nil { 305 var moreDiags tfdiags.Diagnostics 306 state.CheckResults, moreDiags = decodeCheckResultsV4(sV4.CheckResults) 307 diags = diags.Append(moreDiags) 308 } 309 310 file.State = state 311 return file, diags 312 } 313 314 func writeStateV4(file *File, w io.Writer) tfdiags.Diagnostics { 315 // Here we'll convert back from the "File" representation to our 316 // stateV4 struct representation and write that. 317 // 318 // While we support legacy state formats for reading, we only support the 319 // latest for writing and so if a V5 is added in future then this function 320 // should be deleted and replaced with a writeStateV5, even though the 321 // read/prepare V4 functions above would stick around. 322 323 var diags tfdiags.Diagnostics 324 if file == nil || file.State == nil { 325 panic("attempt to write nil state to file") 326 } 327 328 var terraformVersion string 329 if file.TerraformVersion != nil { 330 terraformVersion = file.TerraformVersion.String() 331 } 332 333 sV4 := &stateV4{ 334 TerraformVersion: terraformVersion, 335 Serial: file.Serial, 336 Lineage: file.Lineage, 337 RootOutputs: map[string]outputStateV4{}, 338 Resources: []resourceStateV4{}, 339 } 340 341 for name, os := range file.State.RootModule().OutputValues { 342 src, err := ctyjson.Marshal(os.Value, os.Value.Type()) 343 if err != nil { 344 diags = diags.Append(tfdiags.Sourceless( 345 tfdiags.Error, 346 "Failed to serialize output value in state", 347 fmt.Sprintf("An error occured while serializing output value %q: %s.", name, err), 348 )) 349 continue 350 } 351 352 typeSrc, err := ctyjson.MarshalType(os.Value.Type()) 353 if err != nil { 354 diags = diags.Append(tfdiags.Sourceless( 355 tfdiags.Error, 356 "Failed to serialize output value in state", 357 fmt.Sprintf("An error occured while serializing the type of output value %q: %s.", name, err), 358 )) 359 continue 360 } 361 362 sV4.RootOutputs[name] = outputStateV4{ 363 Sensitive: os.Sensitive, 364 ValueRaw: json.RawMessage(src), 365 ValueTypeRaw: json.RawMessage(typeSrc), 366 } 367 } 368 369 for _, ms := range file.State.Modules { 370 moduleAddr := ms.Addr 371 for _, rs := range ms.Resources { 372 resourceAddr := rs.Addr.Resource 373 374 var mode string 375 switch resourceAddr.Mode { 376 case addrs.ManagedResourceMode: 377 mode = "managed" 378 case addrs.DataResourceMode: 379 mode = "data" 380 default: 381 diags = diags.Append(tfdiags.Sourceless( 382 tfdiags.Error, 383 "Failed to serialize resource in state", 384 fmt.Sprintf("Resource %s has mode %s, which cannot be serialized in state", resourceAddr.Absolute(moduleAddr), resourceAddr.Mode), 385 )) 386 continue 387 } 388 389 sV4.Resources = append(sV4.Resources, resourceStateV4{ 390 Module: moduleAddr.String(), 391 Mode: mode, 392 Type: resourceAddr.Type, 393 Name: resourceAddr.Name, 394 ProviderConfig: rs.ProviderConfig.String(), 395 Instances: []instanceObjectStateV4{}, 396 }) 397 rsV4 := &(sV4.Resources[len(sV4.Resources)-1]) 398 399 for key, is := range rs.Instances { 400 if is.HasCurrent() { 401 var objDiags tfdiags.Diagnostics 402 rsV4.Instances, objDiags = appendInstanceObjectStateV4( 403 rs, is, key, is.Current, states.NotDeposed, 404 rsV4.Instances, 405 ) 406 diags = diags.Append(objDiags) 407 } 408 for dk, obj := range is.Deposed { 409 var objDiags tfdiags.Diagnostics 410 rsV4.Instances, objDiags = appendInstanceObjectStateV4( 411 rs, is, key, obj, dk, 412 rsV4.Instances, 413 ) 414 diags = diags.Append(objDiags) 415 } 416 } 417 } 418 } 419 420 sV4.CheckResults = encodeCheckResultsV4(file.State.CheckResults) 421 422 sV4.normalize() 423 424 src, err := json.MarshalIndent(sV4, "", " ") 425 if err != nil { 426 // Shouldn't happen if we do our conversion to *stateV4 correctly above. 427 diags = diags.Append(tfdiags.Sourceless( 428 tfdiags.Error, 429 "Failed to serialize state", 430 fmt.Sprintf("An error occured while serializing the state to save it. This is a bug in Terraform and should be reported: %s.", err), 431 )) 432 return diags 433 } 434 src = append(src, '\n') 435 436 _, err = w.Write(src) 437 if err != nil { 438 diags = diags.Append(tfdiags.Sourceless( 439 tfdiags.Error, 440 "Failed to write state", 441 fmt.Sprintf("An error occured while writing the serialized state: %s.", err), 442 )) 443 return diags 444 } 445 446 return diags 447 } 448 449 func appendInstanceObjectStateV4(rs *states.Resource, is *states.ResourceInstance, key addrs.InstanceKey, obj *states.ResourceInstanceObjectSrc, deposed states.DeposedKey, isV4s []instanceObjectStateV4) ([]instanceObjectStateV4, tfdiags.Diagnostics) { 450 var diags tfdiags.Diagnostics 451 452 var status string 453 switch obj.Status { 454 case states.ObjectReady: 455 status = "" 456 case states.ObjectTainted: 457 status = "tainted" 458 default: 459 diags = diags.Append(tfdiags.Sourceless( 460 tfdiags.Error, 461 "Failed to serialize resource instance in state", 462 fmt.Sprintf("Instance %s has status %s, which cannot be saved in state.", rs.Addr.Instance(key), obj.Status), 463 )) 464 } 465 466 var privateRaw []byte 467 if len(obj.Private) > 0 { 468 privateRaw = obj.Private 469 } 470 471 deps := make([]string, len(obj.Dependencies)) 472 for i, depAddr := range obj.Dependencies { 473 deps[i] = depAddr.String() 474 } 475 476 var rawKey interface{} 477 switch tk := key.(type) { 478 case addrs.IntKey: 479 rawKey = int(tk) 480 case addrs.StringKey: 481 rawKey = string(tk) 482 default: 483 if key != addrs.NoKey { 484 diags = diags.Append(tfdiags.Sourceless( 485 tfdiags.Error, 486 "Failed to serialize resource instance in state", 487 fmt.Sprintf("Instance %s has an unsupported instance key: %#v.", rs.Addr.Instance(key), key), 488 )) 489 } 490 } 491 492 // Extract paths from path value marks 493 var paths []cty.Path 494 for _, vm := range obj.AttrSensitivePaths { 495 paths = append(paths, vm.Path) 496 } 497 498 // Marshal paths to JSON 499 attributeSensitivePaths, pathsDiags := marshalPaths(paths) 500 diags = diags.Append(pathsDiags) 501 502 return append(isV4s, instanceObjectStateV4{ 503 IndexKey: rawKey, 504 Deposed: string(deposed), 505 Status: status, 506 SchemaVersion: obj.SchemaVersion, 507 AttributesFlat: obj.AttrsFlat, 508 AttributesRaw: obj.AttrsJSON, 509 AttributeSensitivePaths: attributeSensitivePaths, 510 PrivateRaw: privateRaw, 511 Dependencies: deps, 512 CreateBeforeDestroy: obj.CreateBeforeDestroy, 513 }), diags 514 } 515 516 func decodeCheckResultsV4(in []checkResultsV4) (*states.CheckResults, tfdiags.Diagnostics) { 517 var diags tfdiags.Diagnostics 518 519 ret := &states.CheckResults{} 520 if len(in) == 0 { 521 return ret, diags 522 } 523 524 ret.ConfigResults = addrs.MakeMap[addrs.ConfigCheckable, *states.CheckResultAggregate]() 525 for _, aggrIn := range in { 526 objectKind := decodeCheckableObjectKindV4(aggrIn.ObjectKind) 527 if objectKind == addrs.CheckableKindInvalid { 528 diags = diags.Append(fmt.Errorf("unsupported checkable object kind %q", aggrIn.ObjectKind)) 529 continue 530 } 531 532 // Some trickiness here: we only have an address parser for 533 // addrs.Checkable and not for addrs.ConfigCheckable, but that's okay 534 // because once we have an addrs.Checkable we can always derive an 535 // addrs.ConfigCheckable from it, and a ConfigCheckable should always 536 // be the same syntax as a Checkable with no index information and 537 // thus we can reuse the same parser for both here. 538 configAddrProxy, moreDiags := addrs.ParseCheckableStr(objectKind, aggrIn.ConfigAddr) 539 diags = diags.Append(moreDiags) 540 if moreDiags.HasErrors() { 541 continue 542 } 543 configAddr := configAddrProxy.ConfigCheckable() 544 if configAddr.String() != configAddrProxy.String() { 545 // This is how we catch if the config address included index 546 // information that would be allowed in a Checkable but not 547 // in a ConfigCheckable. 548 diags = diags.Append(fmt.Errorf("invalid checkable config address %s", aggrIn.ConfigAddr)) 549 continue 550 } 551 552 aggr := &states.CheckResultAggregate{ 553 Status: decodeCheckStatusV4(aggrIn.Status), 554 } 555 556 if len(aggrIn.Objects) != 0 { 557 aggr.ObjectResults = addrs.MakeMap[addrs.Checkable, *states.CheckResultObject]() 558 for _, objectIn := range aggrIn.Objects { 559 objectAddr, moreDiags := addrs.ParseCheckableStr(objectKind, objectIn.ObjectAddr) 560 diags = diags.Append(moreDiags) 561 if moreDiags.HasErrors() { 562 continue 563 } 564 565 obj := &states.CheckResultObject{ 566 Status: decodeCheckStatusV4(objectIn.Status), 567 FailureMessages: objectIn.FailureMessages, 568 } 569 aggr.ObjectResults.Put(objectAddr, obj) 570 } 571 } 572 573 ret.ConfigResults.Put(configAddr, aggr) 574 } 575 576 return ret, diags 577 } 578 579 func encodeCheckResultsV4(in *states.CheckResults) []checkResultsV4 { 580 // normalize empty and nil sets in the serialized state 581 if in == nil || in.ConfigResults.Len() == 0 { 582 return nil 583 } 584 585 ret := make([]checkResultsV4, 0, in.ConfigResults.Len()) 586 587 for _, configElem := range in.ConfigResults.Elems { 588 configResultsOut := checkResultsV4{ 589 ObjectKind: encodeCheckableObjectKindV4(configElem.Key.CheckableKind()), 590 ConfigAddr: configElem.Key.String(), 591 Status: encodeCheckStatusV4(configElem.Value.Status), 592 } 593 for _, objectElem := range configElem.Value.ObjectResults.Elems { 594 configResultsOut.Objects = append(configResultsOut.Objects, checkResultsObjectV4{ 595 ObjectAddr: objectElem.Key.String(), 596 Status: encodeCheckStatusV4(objectElem.Value.Status), 597 FailureMessages: objectElem.Value.FailureMessages, 598 }) 599 } 600 601 ret = append(ret, configResultsOut) 602 } 603 604 return ret 605 } 606 607 func decodeCheckStatusV4(in string) checks.Status { 608 switch in { 609 case "pass": 610 return checks.StatusPass 611 case "fail": 612 return checks.StatusFail 613 case "error": 614 return checks.StatusError 615 default: 616 // We'll treat anything else as unknown just as a concession to 617 // forward-compatible parsing, in case a later version of Terraform 618 // introduces a new status. 619 return checks.StatusUnknown 620 } 621 } 622 623 func encodeCheckStatusV4(in checks.Status) string { 624 switch in { 625 case checks.StatusPass: 626 return "pass" 627 case checks.StatusFail: 628 return "fail" 629 case checks.StatusError: 630 return "error" 631 case checks.StatusUnknown: 632 return "unknown" 633 default: 634 panic(fmt.Sprintf("unsupported check status %s", in)) 635 } 636 } 637 638 func decodeCheckableObjectKindV4(in string) addrs.CheckableKind { 639 switch in { 640 case "resource": 641 return addrs.CheckableResource 642 case "output": 643 return addrs.CheckableOutputValue 644 case "check": 645 return addrs.CheckableCheck 646 case "var": 647 return addrs.CheckableInputVariable 648 default: 649 // We'll treat anything else as invalid just as a concession to 650 // forward-compatible parsing, in case a later version of Terraform 651 // introduces a new status. 652 return addrs.CheckableKindInvalid 653 } 654 } 655 656 func encodeCheckableObjectKindV4(in addrs.CheckableKind) string { 657 switch in { 658 case addrs.CheckableResource: 659 return "resource" 660 case addrs.CheckableOutputValue: 661 return "output" 662 case addrs.CheckableCheck: 663 return "check" 664 case addrs.CheckableInputVariable: 665 return "var" 666 default: 667 panic(fmt.Sprintf("unsupported checkable object kind %s", in)) 668 } 669 } 670 671 type stateV4 struct { 672 Version stateVersionV4 `json:"version"` 673 TerraformVersion string `json:"terraform_version"` 674 Serial uint64 `json:"serial"` 675 Lineage string `json:"lineage"` 676 RootOutputs map[string]outputStateV4 `json:"outputs"` 677 Resources []resourceStateV4 `json:"resources"` 678 CheckResults []checkResultsV4 `json:"check_results"` 679 } 680 681 // normalize makes some in-place changes to normalize the way items are 682 // stored to ensure that two functionally-equivalent states will be stored 683 // identically. 684 func (s *stateV4) normalize() { 685 sort.Stable(sortResourcesV4(s.Resources)) 686 for _, rs := range s.Resources { 687 sort.Stable(sortInstancesV4(rs.Instances)) 688 } 689 } 690 691 type outputStateV4 struct { 692 ValueRaw json.RawMessage `json:"value"` 693 ValueTypeRaw json.RawMessage `json:"type"` 694 Sensitive bool `json:"sensitive,omitempty"` 695 } 696 697 type resourceStateV4 struct { 698 Module string `json:"module,omitempty"` 699 Mode string `json:"mode"` 700 Type string `json:"type"` 701 Name string `json:"name"` 702 EachMode string `json:"each,omitempty"` 703 ProviderConfig string `json:"provider"` 704 Instances []instanceObjectStateV4 `json:"instances"` 705 } 706 707 type instanceObjectStateV4 struct { 708 IndexKey interface{} `json:"index_key,omitempty"` 709 Status string `json:"status,omitempty"` 710 Deposed string `json:"deposed,omitempty"` 711 712 SchemaVersion uint64 `json:"schema_version"` 713 AttributesRaw json.RawMessage `json:"attributes,omitempty"` 714 AttributesFlat map[string]string `json:"attributes_flat,omitempty"` 715 AttributeSensitivePaths json.RawMessage `json:"sensitive_attributes,omitempty"` 716 717 PrivateRaw []byte `json:"private,omitempty"` 718 719 Dependencies []string `json:"dependencies,omitempty"` 720 721 CreateBeforeDestroy bool `json:"create_before_destroy,omitempty"` 722 } 723 724 type checkResultsV4 struct { 725 ObjectKind string `json:"object_kind"` 726 ConfigAddr string `json:"config_addr"` 727 Status string `json:"status"` 728 Objects []checkResultsObjectV4 `json:"objects"` 729 } 730 731 type checkResultsObjectV4 struct { 732 ObjectAddr string `json:"object_addr"` 733 Status string `json:"status"` 734 FailureMessages []string `json:"failure_messages,omitempty"` 735 } 736 737 // stateVersionV4 is a weird special type we use to produce our hard-coded 738 // "version": 4 in the JSON serialization. 739 type stateVersionV4 struct{} 740 741 func (sv stateVersionV4) MarshalJSON() ([]byte, error) { 742 return []byte{'4'}, nil 743 } 744 745 func (sv stateVersionV4) UnmarshalJSON([]byte) error { 746 // Nothing to do: we already know we're version 4 747 return nil 748 } 749 750 type sortResourcesV4 []resourceStateV4 751 752 func (sr sortResourcesV4) Len() int { return len(sr) } 753 func (sr sortResourcesV4) Swap(i, j int) { sr[i], sr[j] = sr[j], sr[i] } 754 func (sr sortResourcesV4) Less(i, j int) bool { 755 switch { 756 case sr[i].Module != sr[j].Module: 757 return sr[i].Module < sr[j].Module 758 case sr[i].Mode != sr[j].Mode: 759 return sr[i].Mode < sr[j].Mode 760 case sr[i].Type != sr[j].Type: 761 return sr[i].Type < sr[j].Type 762 case sr[i].Name != sr[j].Name: 763 return sr[i].Name < sr[j].Name 764 default: 765 return false 766 } 767 } 768 769 type sortInstancesV4 []instanceObjectStateV4 770 771 func (si sortInstancesV4) Len() int { return len(si) } 772 func (si sortInstancesV4) Swap(i, j int) { si[i], si[j] = si[j], si[i] } 773 func (si sortInstancesV4) Less(i, j int) bool { 774 ki := si[i].IndexKey 775 kj := si[j].IndexKey 776 if ki != kj { 777 if (ki == nil) != (kj == nil) { 778 return ki == nil 779 } 780 if kii, isInt := ki.(int); isInt { 781 if kji, isInt := kj.(int); isInt { 782 return kii < kji 783 } 784 return true 785 } 786 if kis, isStr := ki.(string); isStr { 787 if kjs, isStr := kj.(string); isStr { 788 return kis < kjs 789 } 790 return true 791 } 792 } 793 if si[i].Deposed != si[j].Deposed { 794 return si[i].Deposed < si[j].Deposed 795 } 796 return false 797 } 798 799 // pathStep is an intermediate representation of a cty.PathStep to facilitate 800 // consistent JSON serialization. The Value field can either be a cty.Value of 801 // dynamic type (for index steps), or a string (for get attr steps). 802 type pathStep struct { 803 Type string `json:"type"` 804 Value json.RawMessage `json:"value"` 805 } 806 807 const ( 808 indexPathStepType = "index" 809 getAttrPathStepType = "get_attr" 810 ) 811 812 func unmarshalPaths(buf []byte) ([]cty.Path, tfdiags.Diagnostics) { 813 var diags tfdiags.Diagnostics 814 var jsonPaths [][]pathStep 815 816 err := json.Unmarshal(buf, &jsonPaths) 817 if err != nil { 818 diags = diags.Append(tfdiags.Sourceless( 819 tfdiags.Error, 820 "Error unmarshaling path steps", 821 err.Error(), 822 )) 823 } 824 825 paths := make([]cty.Path, 0, len(jsonPaths)) 826 827 unmarshalOuter: 828 for _, jsonPath := range jsonPaths { 829 var path cty.Path 830 for _, jsonStep := range jsonPath { 831 switch jsonStep.Type { 832 case indexPathStepType: 833 key, err := ctyjson.Unmarshal(jsonStep.Value, cty.DynamicPseudoType) 834 if err != nil { 835 diags = diags.Append(tfdiags.Sourceless( 836 tfdiags.Error, 837 "Error unmarshaling path step", 838 fmt.Sprintf("Failed to unmarshal index step key: %s", err), 839 )) 840 continue unmarshalOuter 841 } 842 path = append(path, cty.IndexStep{Key: key}) 843 case getAttrPathStepType: 844 var name string 845 if err := json.Unmarshal(jsonStep.Value, &name); err != nil { 846 diags = diags.Append(tfdiags.Sourceless( 847 tfdiags.Error, 848 "Error unmarshaling path step", 849 fmt.Sprintf("Failed to unmarshal get attr step name: %s", err), 850 )) 851 continue unmarshalOuter 852 } 853 path = append(path, cty.GetAttrStep{Name: name}) 854 default: 855 diags = diags.Append(tfdiags.Sourceless( 856 tfdiags.Error, 857 "Unsupported path step", 858 fmt.Sprintf("Unsupported path step type %q", jsonStep.Type), 859 )) 860 continue unmarshalOuter 861 } 862 } 863 paths = append(paths, path) 864 } 865 866 return paths, diags 867 } 868 869 func marshalPaths(paths []cty.Path) ([]byte, tfdiags.Diagnostics) { 870 var diags tfdiags.Diagnostics 871 872 // cty.Path is a slice of cty.PathSteps, so our representation of a slice 873 // of paths is a nested slice of our intermediate pathStep struct 874 jsonPaths := make([][]pathStep, 0, len(paths)) 875 876 marshalOuter: 877 for _, path := range paths { 878 jsonPath := make([]pathStep, 0, len(path)) 879 for _, step := range path { 880 var jsonStep pathStep 881 switch s := step.(type) { 882 case cty.IndexStep: 883 key, err := ctyjson.Marshal(s.Key, cty.DynamicPseudoType) 884 if err != nil { 885 diags = diags.Append(tfdiags.Sourceless( 886 tfdiags.Error, 887 "Error marshaling path step", 888 fmt.Sprintf("Failed to marshal index step key %#v: %s", s.Key, err), 889 )) 890 continue marshalOuter 891 } 892 jsonStep.Type = indexPathStepType 893 jsonStep.Value = key 894 case cty.GetAttrStep: 895 name, err := json.Marshal(s.Name) 896 if err != nil { 897 diags = diags.Append(tfdiags.Sourceless( 898 tfdiags.Error, 899 "Error marshaling path step", 900 fmt.Sprintf("Failed to marshal get attr step name %s: %s", s.Name, err), 901 )) 902 continue marshalOuter 903 } 904 jsonStep.Type = getAttrPathStepType 905 jsonStep.Value = name 906 default: 907 diags = diags.Append(tfdiags.Sourceless( 908 tfdiags.Error, 909 "Unsupported path step", 910 fmt.Sprintf("Unsupported path step %#v (%t)", step, step), 911 )) 912 continue marshalOuter 913 } 914 jsonPath = append(jsonPath, jsonStep) 915 } 916 jsonPaths = append(jsonPaths, jsonPath) 917 } 918 919 buf, err := json.Marshal(jsonPaths) 920 if err != nil { 921 diags = diags.Append(tfdiags.Sourceless( 922 tfdiags.Error, 923 "Error marshaling path steps", 924 fmt.Sprintf("Failed to marshal path steps: %s", err), 925 )) 926 } 927 928 return buf, diags 929 }