| 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 |
| } |