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  }