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