github.com/opentofu/opentofu@v1.7.1/internal/states/statefile/version4.go (about)

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