605 lines
17 KiB
Go
605 lines
17 KiB
Go
package statefile
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
|
|
version "github.com/hashicorp/go-version"
|
|
ctyjson "github.com/zclconf/go-cty/cty/json"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/states"
|
|
"github.com/hashicorp/terraform/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() {
|
|
continue
|
|
}
|
|
|
|
var eachMode states.EachMode
|
|
switch rsV4.EachMode {
|
|
case "":
|
|
eachMode = states.NoEach
|
|
case "list":
|
|
eachMode = states.EachList
|
|
case "map":
|
|
eachMode = states.EachMap
|
|
default:
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Invalid resource metadata in state",
|
|
fmt.Sprintf("Resource %s has invalid \"each\" value %q in state.", rAddr.Absolute(moduleAddr), eachMode),
|
|
))
|
|
continue
|
|
}
|
|
|
|
ms := state.EnsureModule(moduleAddr)
|
|
|
|
// Ensure the resource container object is present in the state.
|
|
ms.SetResourceMeta(rAddr, eachMode, 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,
|
|
}
|
|
|
|
{
|
|
// 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{'{', '}'}
|
|
}
|
|
}
|
|
|
|
{
|
|
// 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.Referenceable, 0, len(depsRaw))
|
|
for _, depRaw := range depsRaw {
|
|
ref, refDiags := addrs.ParseRefStr(depRaw)
|
|
diags = diags.Append(refDiags)
|
|
if refDiags.HasErrors() {
|
|
continue
|
|
}
|
|
if len(ref.Remaining) != 0 {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Invalid resource instance metadata in state",
|
|
fmt.Sprintf("Instance %s declares dependency on %q, which is not a reference to a dependable object.", instAddr.Absolute(moduleAddr), depRaw),
|
|
))
|
|
}
|
|
if ref.Subject == nil {
|
|
// Should never happen
|
|
panic(fmt.Sprintf("parsing dependency %q for instance %s returned a nil address", depRaw, instAddr.Absolute(moduleAddr)))
|
|
}
|
|
deps = append(deps, ref.Subject)
|
|
}
|
|
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.SetResourceMeta(rAddr, eachMode, 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{}
|
|
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
|
|
|
|
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
|
|
}
|
|
|
|
var eachMode string
|
|
switch rs.EachMode {
|
|
case states.NoEach:
|
|
eachMode = ""
|
|
case states.EachList:
|
|
eachMode = "list"
|
|
case states.EachMap:
|
|
eachMode = "map"
|
|
default:
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Failed to serialize resource in state",
|
|
fmt.Sprintf("Resource %s has \"each\" mode %s, which cannot be serialized in state", resourceAddr.Absolute(moduleAddr), rs.EachMode),
|
|
))
|
|
continue
|
|
}
|
|
|
|
sV4.Resources = append(sV4.Resources, resourceStateV4{
|
|
Module: moduleAddr.String(),
|
|
Mode: mode,
|
|
Type: resourceAddr.Type,
|
|
Name: resourceAddr.Name,
|
|
EachMode: eachMode,
|
|
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),
|
|
))
|
|
}
|
|
}
|
|
|
|
return append(isV4s, instanceObjectStateV4{
|
|
IndexKey: rawKey,
|
|
Deposed: string(deposed),
|
|
Status: status,
|
|
SchemaVersion: obj.SchemaVersion,
|
|
AttributesFlat: obj.AttrsFlat,
|
|
AttributesRaw: obj.AttrsJSON,
|
|
PrivateRaw: privateRaw,
|
|
Dependencies: deps,
|
|
}), 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"`
|
|
|
|
PrivateRaw []byte `json:"private,omitempty"`
|
|
|
|
Dependencies []string `json:"depends_on,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].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
|
|
}
|