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