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/lang/marks" "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(marks.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 }