github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/states/statefile/version4.go (about)

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