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