terraform/internal/states/statefile/version4.go

743 lines
21 KiB
Go

package statefile
import (
"encoding/json"
"fmt"
"io"
"sort"
version "github.com/hashicorp/go-version"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
)
func readStateV4(src []byte) (*File, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
sV4 := &stateV4{}
err := json.Unmarshal(src, sV4)
if err != nil {
diags = diags.Append(jsonUnmarshalDiags(err))
return nil, diags
}
file, prepDiags := prepareStateV4(sV4)
diags = diags.Append(prepDiags)
return file, diags
}
func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var tfVersion *version.Version
if sV4.TerraformVersion != "" {
var err error
tfVersion, err = version.NewVersion(sV4.TerraformVersion)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid Terraform version string",
fmt.Sprintf("State file claims to have been written by Terraform version %q, which is not a valid version string.", sV4.TerraformVersion),
))
}
}
file := &File{
TerraformVersion: tfVersion,
Serial: sV4.Serial,
Lineage: sV4.Lineage,
}
state := states.NewState()
for _, rsV4 := range sV4.Resources {
rAddr := addrs.Resource{
Type: rsV4.Type,
Name: rsV4.Name,
}
switch rsV4.Mode {
case "managed":
rAddr.Mode = addrs.ManagedResourceMode
case "data":
rAddr.Mode = addrs.DataResourceMode
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid resource mode in state",
fmt.Sprintf("State contains a resource with mode %q (%q %q) which is not supported.", rsV4.Mode, rAddr.Type, rAddr.Name),
))
continue
}
moduleAddr := addrs.RootModuleInstance
if rsV4.Module != "" {
var addrDiags tfdiags.Diagnostics
moduleAddr, addrDiags = addrs.ParseModuleInstanceStr(rsV4.Module)
diags = diags.Append(addrDiags)
if addrDiags.HasErrors() {
continue
}
}
providerAddr, addrDiags := addrs.ParseAbsProviderConfigStr(rsV4.ProviderConfig)
diags.Append(addrDiags)
if addrDiags.HasErrors() {
// If ParseAbsProviderConfigStr returns an error, the state may have
// been written before Provider FQNs were introduced and the
// AbsProviderConfig string format will need normalization. If so,
// we treat it like a legacy provider (namespace "-") and let the
// provider installer handle detecting the FQN.
var legacyAddrDiags tfdiags.Diagnostics
providerAddr, legacyAddrDiags = addrs.ParseLegacyAbsProviderConfigStr(rsV4.ProviderConfig)
if legacyAddrDiags.HasErrors() {
continue
}
}
ms := state.EnsureModule(moduleAddr)
// Ensure the resource container object is present in the state.
ms.SetResourceProvider(rAddr, providerAddr)
for _, isV4 := range rsV4.Instances {
keyRaw := isV4.IndexKey
var key addrs.InstanceKey
switch tk := keyRaw.(type) {
case int:
key = addrs.IntKey(tk)
case float64:
// Since JSON only has one number type, reading from encoding/json
// gives us a float64 here even if the number is whole.
// float64 has a smaller integer range than int, but in practice
// we rarely have more than a few tens of instances and so
// it's unlikely that we'll exhaust the 52 bits in a float64.
key = addrs.IntKey(int(tk))
case string:
key = addrs.StringKey(tk)
default:
if keyRaw != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid resource instance metadata in state",
fmt.Sprintf("Resource %s has an instance with the invalid instance key %#v.", rAddr.Absolute(moduleAddr), keyRaw),
))
continue
}
key = addrs.NoKey
}
instAddr := rAddr.Instance(key)
obj := &states.ResourceInstanceObjectSrc{
SchemaVersion: isV4.SchemaVersion,
CreateBeforeDestroy: isV4.CreateBeforeDestroy,
}
{
// Instance attributes
switch {
case isV4.AttributesRaw != nil:
obj.AttrsJSON = isV4.AttributesRaw
case isV4.AttributesFlat != nil:
obj.AttrsFlat = isV4.AttributesFlat
default:
// This is odd, but we'll accept it and just treat the
// object has being empty. In practice this should arise
// only from the contrived sort of state objects we tend
// to hand-write inline in tests.
obj.AttrsJSON = []byte{'{', '}'}
}
}
// Sensitive paths
if isV4.AttributeSensitivePaths != nil {
paths, pathsDiags := unmarshalPaths([]byte(isV4.AttributeSensitivePaths))
diags = diags.Append(pathsDiags)
if pathsDiags.HasErrors() {
continue
}
var pvm []cty.PathValueMarks
for _, path := range paths {
pvm = append(pvm, cty.PathValueMarks{
Path: path,
Marks: cty.NewValueMarks("sensitive"),
})
}
obj.AttrSensitivePaths = pvm
}
{
// Status
raw := isV4.Status
switch raw {
case "":
obj.Status = states.ObjectReady
case "tainted":
obj.Status = states.ObjectTainted
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid resource instance metadata in state",
fmt.Sprintf("Instance %s has invalid status %q.", instAddr.Absolute(moduleAddr), raw),
))
continue
}
}
if raw := isV4.PrivateRaw; len(raw) > 0 {
obj.Private = raw
}
{
depsRaw := isV4.Dependencies
deps := make([]addrs.ConfigResource, 0, len(depsRaw))
for _, depRaw := range depsRaw {
addr, addrDiags := addrs.ParseAbsResourceStr(depRaw)
diags = diags.Append(addrDiags)
if addrDiags.HasErrors() {
continue
}
deps = append(deps, addr.Config())
}
obj.Dependencies = deps
}
switch {
case isV4.Deposed != "":
dk := states.DeposedKey(isV4.Deposed)
if len(dk) != 8 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid resource instance metadata in state",
fmt.Sprintf("Instance %s has an object with deposed key %q, which is not correctly formatted.", instAddr.Absolute(moduleAddr), isV4.Deposed),
))
continue
}
is := ms.ResourceInstance(instAddr)
if is.HasDeposed(dk) {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Duplicate resource instance in state",
fmt.Sprintf("Instance %s deposed object %q appears multiple times in the state file.", instAddr.Absolute(moduleAddr), dk),
))
continue
}
ms.SetResourceInstanceDeposed(instAddr, dk, obj, providerAddr)
default:
is := ms.ResourceInstance(instAddr)
if is.HasCurrent() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Duplicate resource instance in state",
fmt.Sprintf("Instance %s appears multiple times in the state file.", instAddr.Absolute(moduleAddr)),
))
continue
}
ms.SetResourceInstanceCurrent(instAddr, obj, providerAddr)
}
}
// We repeat this after creating the instances because
// SetResourceInstanceCurrent automatically resets this metadata based
// on the incoming objects. That behavior is useful when we're making
// piecemeal updates to the state during an apply, but when we're
// reading the state file we want to reflect its contents exactly.
ms.SetResourceProvider(rAddr, providerAddr)
}
// The root module is special in that we persist its attributes and thus
// need to reload them now. (For descendent modules we just re-calculate
// them based on the latest configuration on each run.)
{
rootModule := state.RootModule()
for name, fos := range sV4.RootOutputs {
os := &states.OutputValue{
Addr: addrs.AbsOutputValue{
OutputValue: addrs.OutputValue{
Name: name,
},
},
}
os.Sensitive = fos.Sensitive
ty, err := ctyjson.UnmarshalType([]byte(fos.ValueTypeRaw))
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid output value type in state",
fmt.Sprintf("The state file has an invalid type specification for output %q: %s.", name, err),
))
continue
}
val, err := ctyjson.Unmarshal([]byte(fos.ValueRaw), ty)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid output value saved in state",
fmt.Sprintf("The state file has an invalid value for output %q: %s.", name, err),
))
continue
}
os.Value = val
rootModule.OutputValues[name] = os
}
}
file.State = state
return file, diags
}
func writeStateV4(file *File, w io.Writer) tfdiags.Diagnostics {
// Here we'll convert back from the "File" representation to our
// stateV4 struct representation and write that.
//
// While we support legacy state formats for reading, we only support the
// latest for writing and so if a V5 is added in future then this function
// should be deleted and replaced with a writeStateV5, even though the
// read/prepare V4 functions above would stick around.
var diags tfdiags.Diagnostics
if file == nil || file.State == nil {
panic("attempt to write nil state to file")
}
var terraformVersion string
if file.TerraformVersion != nil {
terraformVersion = file.TerraformVersion.String()
}
sV4 := &stateV4{
TerraformVersion: terraformVersion,
Serial: file.Serial,
Lineage: file.Lineage,
RootOutputs: map[string]outputStateV4{},
Resources: []resourceStateV4{},
}
for name, os := range file.State.RootModule().OutputValues {
src, err := ctyjson.Marshal(os.Value, os.Value.Type())
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to serialize output value in state",
fmt.Sprintf("An error occured while serializing output value %q: %s.", name, err),
))
continue
}
typeSrc, err := ctyjson.MarshalType(os.Value.Type())
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to serialize output value in state",
fmt.Sprintf("An error occured while serializing the type of output value %q: %s.", name, err),
))
continue
}
sV4.RootOutputs[name] = outputStateV4{
Sensitive: os.Sensitive,
ValueRaw: json.RawMessage(src),
ValueTypeRaw: json.RawMessage(typeSrc),
}
}
for _, ms := range file.State.Modules {
moduleAddr := ms.Addr
for _, rs := range ms.Resources {
resourceAddr := rs.Addr.Resource
var mode string
switch resourceAddr.Mode {
case addrs.ManagedResourceMode:
mode = "managed"
case addrs.DataResourceMode:
mode = "data"
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to serialize resource in state",
fmt.Sprintf("Resource %s has mode %s, which cannot be serialized in state", resourceAddr.Absolute(moduleAddr), resourceAddr.Mode),
))
continue
}
sV4.Resources = append(sV4.Resources, resourceStateV4{
Module: moduleAddr.String(),
Mode: mode,
Type: resourceAddr.Type,
Name: resourceAddr.Name,
ProviderConfig: rs.ProviderConfig.String(),
Instances: []instanceObjectStateV4{},
})
rsV4 := &(sV4.Resources[len(sV4.Resources)-1])
for key, is := range rs.Instances {
if is.HasCurrent() {
var objDiags tfdiags.Diagnostics
rsV4.Instances, objDiags = appendInstanceObjectStateV4(
rs, is, key, is.Current, states.NotDeposed,
rsV4.Instances,
)
diags = diags.Append(objDiags)
}
for dk, obj := range is.Deposed {
var objDiags tfdiags.Diagnostics
rsV4.Instances, objDiags = appendInstanceObjectStateV4(
rs, is, key, obj, dk,
rsV4.Instances,
)
diags = diags.Append(objDiags)
}
}
}
}
sV4.normalize()
src, err := json.MarshalIndent(sV4, "", " ")
if err != nil {
// Shouldn't happen if we do our conversion to *stateV4 correctly above.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to serialize state",
fmt.Sprintf("An error occured while serializing the state to save it. This is a bug in Terraform and should be reported: %s.", err),
))
return diags
}
src = append(src, '\n')
_, err = w.Write(src)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to write state",
fmt.Sprintf("An error occured while writing the serialized state: %s.", err),
))
return diags
}
return diags
}
func appendInstanceObjectStateV4(rs *states.Resource, is *states.ResourceInstance, key addrs.InstanceKey, obj *states.ResourceInstanceObjectSrc, deposed states.DeposedKey, isV4s []instanceObjectStateV4) ([]instanceObjectStateV4, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var status string
switch obj.Status {
case states.ObjectReady:
status = ""
case states.ObjectTainted:
status = "tainted"
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to serialize resource instance in state",
fmt.Sprintf("Instance %s has status %s, which cannot be saved in state.", rs.Addr.Instance(key), obj.Status),
))
}
var privateRaw []byte
if len(obj.Private) > 0 {
privateRaw = obj.Private
}
deps := make([]string, len(obj.Dependencies))
for i, depAddr := range obj.Dependencies {
deps[i] = depAddr.String()
}
var rawKey interface{}
switch tk := key.(type) {
case addrs.IntKey:
rawKey = int(tk)
case addrs.StringKey:
rawKey = string(tk)
default:
if key != addrs.NoKey {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to serialize resource instance in state",
fmt.Sprintf("Instance %s has an unsupported instance key: %#v.", rs.Addr.Instance(key), key),
))
}
}
// Extract paths from path value marks
var paths []cty.Path
for _, vm := range obj.AttrSensitivePaths {
paths = append(paths, vm.Path)
}
// Marshal paths to JSON
attributeSensitivePaths, pathsDiags := marshalPaths(paths)
diags = diags.Append(pathsDiags)
return append(isV4s, instanceObjectStateV4{
IndexKey: rawKey,
Deposed: string(deposed),
Status: status,
SchemaVersion: obj.SchemaVersion,
AttributesFlat: obj.AttrsFlat,
AttributesRaw: obj.AttrsJSON,
AttributeSensitivePaths: attributeSensitivePaths,
PrivateRaw: privateRaw,
Dependencies: deps,
CreateBeforeDestroy: obj.CreateBeforeDestroy,
}), diags
}
type stateV4 struct {
Version stateVersionV4 `json:"version"`
TerraformVersion string `json:"terraform_version"`
Serial uint64 `json:"serial"`
Lineage string `json:"lineage"`
RootOutputs map[string]outputStateV4 `json:"outputs"`
Resources []resourceStateV4 `json:"resources"`
}
// normalize makes some in-place changes to normalize the way items are
// stored to ensure that two functionally-equivalent states will be stored
// identically.
func (s *stateV4) normalize() {
sort.Stable(sortResourcesV4(s.Resources))
for _, rs := range s.Resources {
sort.Stable(sortInstancesV4(rs.Instances))
}
}
type outputStateV4 struct {
ValueRaw json.RawMessage `json:"value"`
ValueTypeRaw json.RawMessage `json:"type"`
Sensitive bool `json:"sensitive,omitempty"`
}
type resourceStateV4 struct {
Module string `json:"module,omitempty"`
Mode string `json:"mode"`
Type string `json:"type"`
Name string `json:"name"`
EachMode string `json:"each,omitempty"`
ProviderConfig string `json:"provider"`
Instances []instanceObjectStateV4 `json:"instances"`
}
type instanceObjectStateV4 struct {
IndexKey interface{} `json:"index_key,omitempty"`
Status string `json:"status,omitempty"`
Deposed string `json:"deposed,omitempty"`
SchemaVersion uint64 `json:"schema_version"`
AttributesRaw json.RawMessage `json:"attributes,omitempty"`
AttributesFlat map[string]string `json:"attributes_flat,omitempty"`
AttributeSensitivePaths json.RawMessage `json:"sensitive_attributes,omitempty,"`
PrivateRaw []byte `json:"private,omitempty"`
Dependencies []string `json:"dependencies,omitempty"`
CreateBeforeDestroy bool `json:"create_before_destroy,omitempty"`
}
// stateVersionV4 is a weird special type we use to produce our hard-coded
// "version": 4 in the JSON serialization.
type stateVersionV4 struct{}
func (sv stateVersionV4) MarshalJSON() ([]byte, error) {
return []byte{'4'}, nil
}
func (sv stateVersionV4) UnmarshalJSON([]byte) error {
// Nothing to do: we already know we're version 4
return nil
}
type sortResourcesV4 []resourceStateV4
func (sr sortResourcesV4) Len() int { return len(sr) }
func (sr sortResourcesV4) Swap(i, j int) { sr[i], sr[j] = sr[j], sr[i] }
func (sr sortResourcesV4) Less(i, j int) bool {
switch {
case sr[i].Module != sr[j].Module:
return sr[i].Module < sr[j].Module
case sr[i].Mode != sr[j].Mode:
return sr[i].Mode < sr[j].Mode
case sr[i].Type != sr[j].Type:
return sr[i].Type < sr[j].Type
case sr[i].Name != sr[j].Name:
return sr[i].Name < sr[j].Name
default:
return false
}
}
type sortInstancesV4 []instanceObjectStateV4
func (si sortInstancesV4) Len() int { return len(si) }
func (si sortInstancesV4) Swap(i, j int) { si[i], si[j] = si[j], si[i] }
func (si sortInstancesV4) Less(i, j int) bool {
ki := si[i].IndexKey
kj := si[j].IndexKey
if ki != kj {
if (ki == nil) != (kj == nil) {
return ki == nil
}
if kii, isInt := ki.(int); isInt {
if kji, isInt := kj.(int); isInt {
return kii < kji
}
return true
}
if kis, isStr := ki.(string); isStr {
if kjs, isStr := kj.(string); isStr {
return kis < kjs
}
return true
}
}
if si[i].Deposed != si[j].Deposed {
return si[i].Deposed < si[j].Deposed
}
return false
}
// pathStep is an intermediate representation of a cty.PathStep to facilitate
// consistent JSON serialization. The Value field can either be a cty.Value of
// dynamic type (for index steps), or a string (for get attr steps).
type pathStep struct {
Type string `json:"type"`
Value json.RawMessage `json:"value"`
}
const (
indexPathStepType = "index"
getAttrPathStepType = "get_attr"
)
func unmarshalPaths(buf []byte) ([]cty.Path, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var jsonPaths [][]pathStep
err := json.Unmarshal(buf, &jsonPaths)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Error unmarshaling path steps",
err.Error(),
))
}
paths := make([]cty.Path, 0, len(jsonPaths))
unmarshalOuter:
for _, jsonPath := range jsonPaths {
var path cty.Path
for _, jsonStep := range jsonPath {
switch jsonStep.Type {
case indexPathStepType:
key, err := ctyjson.Unmarshal(jsonStep.Value, cty.DynamicPseudoType)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Error unmarshaling path step",
fmt.Sprintf("Failed to unmarshal index step key: %s", err),
))
continue unmarshalOuter
}
path = append(path, cty.IndexStep{Key: key})
case getAttrPathStepType:
var name string
if err := json.Unmarshal(jsonStep.Value, &name); err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Error unmarshaling path step",
fmt.Sprintf("Failed to unmarshal get attr step name: %s", err),
))
continue unmarshalOuter
}
path = append(path, cty.GetAttrStep{Name: name})
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unsupported path step",
fmt.Sprintf("Unsupported path step type %q", jsonStep.Type),
))
continue unmarshalOuter
}
}
paths = append(paths, path)
}
return paths, diags
}
func marshalPaths(paths []cty.Path) ([]byte, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// cty.Path is a slice of cty.PathSteps, so our representation of a slice
// of paths is a nested slice of our intermediate pathStep struct
jsonPaths := make([][]pathStep, 0, len(paths))
marshalOuter:
for _, path := range paths {
jsonPath := make([]pathStep, 0, len(path))
for _, step := range path {
var jsonStep pathStep
switch s := step.(type) {
case cty.IndexStep:
key, err := ctyjson.Marshal(s.Key, cty.DynamicPseudoType)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Error marshaling path step",
fmt.Sprintf("Failed to marshal index step key %#v: %s", s.Key, err),
))
continue marshalOuter
}
jsonStep.Type = indexPathStepType
jsonStep.Value = key
case cty.GetAttrStep:
name, err := json.Marshal(s.Name)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Error marshaling path step",
fmt.Sprintf("Failed to marshal get attr step name %s: %s", s.Name, err),
))
continue marshalOuter
}
jsonStep.Type = getAttrPathStepType
jsonStep.Value = name
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unsupported path step",
fmt.Sprintf("Unsupported path step %#v (%t)", step, step),
))
continue marshalOuter
}
jsonPath = append(jsonPath, jsonStep)
}
jsonPaths = append(jsonPaths, jsonPath)
}
buf, err := json.Marshal(jsonPaths)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Error marshaling path steps",
fmt.Sprintf("Failed to marshal path steps: %s", err),
))
}
return buf, diags
}