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