| package statefile |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "strconv" |
| "strings" |
| |
| "github.com/hashicorp/hcl/v2/hclsyntax" |
| "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/configs" |
| "github.com/hashicorp/terraform/internal/states" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| ) |
| |
| func upgradeStateV3ToV4(old *stateV3) (*stateV4, error) { |
| |
| if old.Serial < 0 { |
| // The new format is using uint64 here, which should be fine for any |
| // real state (we only used positive integers in practice) but we'll |
| // catch this explicitly here to avoid weird behavior if a state file |
| // has been tampered with in some way. |
| return nil, fmt.Errorf("state has serial less than zero, which is invalid") |
| } |
| |
| new := &stateV4{ |
| TerraformVersion: old.TFVersion, |
| Serial: uint64(old.Serial), |
| Lineage: old.Lineage, |
| RootOutputs: map[string]outputStateV4{}, |
| Resources: []resourceStateV4{}, |
| } |
| |
| if new.TerraformVersion == "" { |
| // Older formats considered this to be optional, but now it's required |
| // and so we'll stub it out with something that's definitely older |
| // than the version that really created this state. |
| new.TerraformVersion = "0.0.0" |
| } |
| |
| for _, msOld := range old.Modules { |
| if len(msOld.Path) < 1 || msOld.Path[0] != "root" { |
| return nil, fmt.Errorf("state contains invalid module path %#v", msOld.Path) |
| } |
| |
| // Convert legacy-style module address into our newer address type. |
| // Since these old formats are only generated by versions of Terraform |
| // that don't support count and for_each on modules, we can just assume |
| // all of the modules are unkeyed. |
| moduleAddr := make(addrs.ModuleInstance, len(msOld.Path)-1) |
| for i, name := range msOld.Path[1:] { |
| if !hclsyntax.ValidIdentifier(name) { |
| // If we don't fail here then we'll produce an invalid state |
| // version 4 which subsequent operations will reject, so we'll |
| // fail early here for safety to make sure we can never |
| // inadvertently commit an invalid snapshot to a backend. |
| return nil, fmt.Errorf("state contains invalid module path %#v: %q is not a valid identifier; rename it in Terraform 0.11 before upgrading to Terraform 0.12", msOld.Path, name) |
| } |
| moduleAddr[i] = addrs.ModuleInstanceStep{ |
| Name: name, |
| InstanceKey: addrs.NoKey, |
| } |
| } |
| |
| // In a v3 state file, a "resource state" is actually an instance |
| // state, so we need to fill in a missing level of hierarchy here |
| // by lazily creating resource states as we encounter them. |
| // We'll track them in here, keyed on the string representation of |
| // the resource address. |
| resourceStates := map[string]*resourceStateV4{} |
| |
| for legacyAddr, rsOld := range msOld.Resources { |
| instAddr, err := parseLegacyResourceAddress(legacyAddr) |
| if err != nil { |
| return nil, err |
| } |
| |
| resAddr := instAddr.Resource |
| rs, exists := resourceStates[resAddr.String()] |
| if !exists { |
| var modeStr string |
| switch resAddr.Mode { |
| case addrs.ManagedResourceMode: |
| modeStr = "managed" |
| case addrs.DataResourceMode: |
| modeStr = "data" |
| default: |
| return nil, fmt.Errorf("state contains resource %s with an unsupported resource mode %#v", resAddr, resAddr.Mode) |
| } |
| |
| // In state versions prior to 4 we allowed each instance of a |
| // resource to have its own provider configuration address, |
| // which makes no real sense in practice because providers |
| // are associated with resources in the configuration. We |
| // elevate that to the resource level during this upgrade, |
| // implicitly taking the provider address of the first instance |
| // we encounter for each resource. While this is lossy in |
| // theory, in practice there is no reason for these values to |
| // differ between instances. |
| var providerAddr addrs.AbsProviderConfig |
| oldProviderAddr := rsOld.Provider |
| if strings.Contains(oldProviderAddr, "provider.") { |
| // Smells like a new-style provider address, but we'll test it. |
| var diags tfdiags.Diagnostics |
| providerAddr, diags = addrs.ParseLegacyAbsProviderConfigStr(oldProviderAddr) |
| if diags.HasErrors() { |
| if strings.Contains(oldProviderAddr, "${") { |
| // There seems to be a common misconception that |
| // interpolation was valid in provider aliases |
| // in 0.11, so we'll use a specialized error |
| // message for that case. |
| return nil, fmt.Errorf("invalid provider config reference %q for %s: this alias seems to contain a template interpolation sequence, which was not supported but also not error-checked in Terraform 0.11. To proceed, rename the associated provider alias to a valid identifier and apply the change with Terraform 0.11 before upgrading to Terraform 0.12", oldProviderAddr, instAddr) |
| } |
| return nil, fmt.Errorf("invalid provider config reference %q for %s: %s", oldProviderAddr, instAddr, diags.Err()) |
| } |
| } else { |
| // Smells like an old-style module-local provider address, |
| // which we'll need to migrate. We'll assume it's referring |
| // to the same module the resource is in, which might be |
| // incorrect but it'll get fixed up next time any updates |
| // are made to an instance. |
| if oldProviderAddr != "" { |
| localAddr, diags := configs.ParseProviderConfigCompactStr(oldProviderAddr) |
| if diags.HasErrors() { |
| if strings.Contains(oldProviderAddr, "${") { |
| // There seems to be a common misconception that |
| // interpolation was valid in provider aliases |
| // in 0.11, so we'll use a specialized error |
| // message for that case. |
| return nil, fmt.Errorf("invalid legacy provider config reference %q for %s: this alias seems to contain a template interpolation sequence, which was not supported but also not error-checked in Terraform 0.11. To proceed, rename the associated provider alias to a valid identifier and apply the change with Terraform 0.11 before upgrading to Terraform 0.12", oldProviderAddr, instAddr) |
| } |
| return nil, fmt.Errorf("invalid legacy provider config reference %q for %s: %s", oldProviderAddr, instAddr, diags.Err()) |
| } |
| providerAddr = addrs.AbsProviderConfig{ |
| Module: moduleAddr.Module(), |
| // We use NewLegacyProvider here so we can use |
| // LegacyString() below to get the appropriate |
| // legacy-style provider string. |
| Provider: addrs.NewLegacyProvider(localAddr.LocalName), |
| Alias: localAddr.Alias, |
| } |
| } else { |
| providerAddr = addrs.AbsProviderConfig{ |
| Module: moduleAddr.Module(), |
| // We use NewLegacyProvider here so we can use |
| // LegacyString() below to get the appropriate |
| // legacy-style provider string. |
| Provider: addrs.NewLegacyProvider(resAddr.ImpliedProvider()), |
| } |
| } |
| } |
| |
| rs = &resourceStateV4{ |
| Module: moduleAddr.String(), |
| Mode: modeStr, |
| Type: resAddr.Type, |
| Name: resAddr.Name, |
| Instances: []instanceObjectStateV4{}, |
| ProviderConfig: providerAddr.LegacyString(), |
| } |
| resourceStates[resAddr.String()] = rs |
| } |
| |
| // Now we'll deal with the instance itself, which may either be |
| // the first instance in a resource we just created or an additional |
| // instance for a resource added on a prior loop. |
| instKey := instAddr.Key |
| if isOld := rsOld.Primary; isOld != nil { |
| isNew, err := upgradeInstanceObjectV3ToV4(rsOld, isOld, instKey, states.NotDeposed) |
| if err != nil { |
| return nil, fmt.Errorf("failed to migrate primary generation of %s: %s", instAddr, err) |
| } |
| rs.Instances = append(rs.Instances, *isNew) |
| } |
| for i, isOld := range rsOld.Deposed { |
| // When we migrate old instances we'll use sequential deposed |
| // keys just so that the upgrade result is deterministic. New |
| // deposed keys allocated moving forward will be pseudorandomly |
| // selected, but we check for collisions and so these |
| // non-random ones won't hurt. |
| deposedKey := states.DeposedKey(fmt.Sprintf("%08x", i+1)) |
| isNew, err := upgradeInstanceObjectV3ToV4(rsOld, isOld, instKey, deposedKey) |
| if err != nil { |
| return nil, fmt.Errorf("failed to migrate deposed generation index %d of %s: %s", i, instAddr, err) |
| } |
| rs.Instances = append(rs.Instances, *isNew) |
| } |
| |
| if instKey != addrs.NoKey && rs.EachMode == "" { |
| rs.EachMode = "list" |
| } |
| } |
| |
| for _, rs := range resourceStates { |
| new.Resources = append(new.Resources, *rs) |
| } |
| |
| if len(msOld.Path) == 1 && msOld.Path[0] == "root" { |
| // We'll migrate the outputs for this module too, then. |
| for name, oldOS := range msOld.Outputs { |
| newOS := outputStateV4{ |
| Sensitive: oldOS.Sensitive, |
| } |
| |
| valRaw := oldOS.Value |
| valSrc, err := json.Marshal(valRaw) |
| if err != nil { |
| // Should never happen, because this value came from JSON |
| // in the first place and so we're just round-tripping here. |
| return nil, fmt.Errorf("failed to serialize output %q value as JSON: %s", name, err) |
| } |
| |
| // The "type" field in state V2 wasn't really that useful |
| // since it was only able to capture string vs. list vs. map. |
| // For this reason, during upgrade we'll just discard it |
| // altogether and use cty's idea of the implied type of |
| // turning our old value into JSON. |
| ty, err := ctyjson.ImpliedType(valSrc) |
| if err != nil { |
| // REALLY should never happen, because we literally just |
| // encoded this as JSON above! |
| return nil, fmt.Errorf("failed to parse output %q value from JSON: %s", name, err) |
| } |
| |
| // ImpliedType tends to produce structural types, but since older |
| // version of Terraform didn't support those a collection type |
| // is probably what was intended, so we'll see if we can |
| // interpret our value as one. |
| ty = simplifyImpliedValueType(ty) |
| |
| tySrc, err := ctyjson.MarshalType(ty) |
| if err != nil { |
| return nil, fmt.Errorf("failed to serialize output %q type as JSON: %s", name, err) |
| } |
| |
| newOS.ValueRaw = json.RawMessage(valSrc) |
| newOS.ValueTypeRaw = json.RawMessage(tySrc) |
| |
| new.RootOutputs[name] = newOS |
| } |
| } |
| } |
| |
| new.normalize() |
| |
| return new, nil |
| } |
| |
| func upgradeInstanceObjectV3ToV4(rsOld *resourceStateV2, isOld *instanceStateV2, instKey addrs.InstanceKey, deposedKey states.DeposedKey) (*instanceObjectStateV4, error) { |
| |
| // Schema versions were, in prior formats, a private concern of the provider |
| // SDK, and not a first-class concept in the state format. Here we're |
| // sniffing for the pre-0.12 SDK's way of representing schema versions |
| // and promoting it to our first-class field if we find it. We'll ignore |
| // it if it doesn't look like what the SDK would've written. If this |
| // sniffing fails then we'll assume schema version 0. |
| var schemaVersion uint64 |
| migratedSchemaVersion := false |
| if raw, exists := isOld.Meta["schema_version"]; exists { |
| switch tv := raw.(type) { |
| case string: |
| v, err := strconv.ParseUint(tv, 10, 64) |
| if err == nil { |
| schemaVersion = v |
| migratedSchemaVersion = true |
| } |
| case int: |
| schemaVersion = uint64(tv) |
| migratedSchemaVersion = true |
| case float64: |
| schemaVersion = uint64(tv) |
| migratedSchemaVersion = true |
| } |
| } |
| |
| private := map[string]interface{}{} |
| for k, v := range isOld.Meta { |
| if k == "schema_version" && migratedSchemaVersion { |
| // We're gonna promote this into our first-class schema version field |
| continue |
| } |
| private[k] = v |
| } |
| var privateJSON []byte |
| if len(private) != 0 { |
| var err error |
| privateJSON, err = json.Marshal(private) |
| if err != nil { |
| // This shouldn't happen, because the Meta values all came from JSON |
| // originally anyway. |
| return nil, fmt.Errorf("cannot serialize private instance object data: %s", err) |
| } |
| } |
| |
| var status string |
| if isOld.Tainted { |
| status = "tainted" |
| } |
| |
| var instKeyRaw interface{} |
| switch tk := instKey.(type) { |
| case addrs.IntKey: |
| instKeyRaw = int(tk) |
| case addrs.StringKey: |
| instKeyRaw = string(tk) |
| default: |
| if instKeyRaw != nil { |
| return nil, fmt.Errorf("unsupported instance key: %#v", instKey) |
| } |
| } |
| |
| var attributes map[string]string |
| if isOld.Attributes != nil { |
| attributes = make(map[string]string, len(isOld.Attributes)) |
| for k, v := range isOld.Attributes { |
| attributes[k] = v |
| } |
| } |
| if isOld.ID != "" { |
| // As a special case, if we don't already have an "id" attribute and |
| // yet there's a non-empty first-class ID on the old object then we'll |
| // create a synthetic id attribute to avoid losing that first-class id. |
| // In practice this generally arises only in tests where state literals |
| // are hand-written in a non-standard way; real code prior to 0.12 |
| // would always force the first-class ID to be copied into the |
| // id attribute before storing. |
| if attributes == nil { |
| attributes = make(map[string]string, len(isOld.Attributes)) |
| } |
| if idVal := attributes["id"]; idVal == "" { |
| attributes["id"] = isOld.ID |
| } |
| } |
| |
| return &instanceObjectStateV4{ |
| IndexKey: instKeyRaw, |
| Status: status, |
| Deposed: string(deposedKey), |
| AttributesFlat: attributes, |
| SchemaVersion: schemaVersion, |
| PrivateRaw: privateJSON, |
| }, nil |
| } |
| |
| // parseLegacyResourceAddress parses the different identifier format used |
| // state formats before version 4, like "instance.name.0". |
| func parseLegacyResourceAddress(s string) (addrs.ResourceInstance, error) { |
| var ret addrs.ResourceInstance |
| |
| // Split based on ".". Every resource address should have at least two |
| // elements (type and name). |
| parts := strings.Split(s, ".") |
| if len(parts) < 2 || len(parts) > 4 { |
| return ret, fmt.Errorf("invalid internal resource address format: %s", s) |
| } |
| |
| // Data resource if we have at least 3 parts and the first one is data |
| ret.Resource.Mode = addrs.ManagedResourceMode |
| if len(parts) > 2 && parts[0] == "data" { |
| ret.Resource.Mode = addrs.DataResourceMode |
| parts = parts[1:] |
| } |
| |
| // If we're not a data resource and we have more than 3, then it is an error |
| if len(parts) > 3 && ret.Resource.Mode != addrs.DataResourceMode { |
| return ret, fmt.Errorf("invalid internal resource address format: %s", s) |
| } |
| |
| // Build the parts of the resource address that are guaranteed to exist |
| ret.Resource.Type = parts[0] |
| ret.Resource.Name = parts[1] |
| ret.Key = addrs.NoKey |
| |
| // If we have more parts, then we have an index. Parse that. |
| if len(parts) > 2 { |
| idx, err := strconv.ParseInt(parts[2], 0, 0) |
| if err != nil { |
| return ret, fmt.Errorf("error parsing resource address %q: %s", s, err) |
| } |
| |
| ret.Key = addrs.IntKey(idx) |
| } |
| |
| return ret, nil |
| } |
| |
| // simplifyImpliedValueType attempts to heuristically simplify a value type |
| // derived from a legacy stored output value into something simpler that |
| // is closer to what would've fitted into the pre-v0.12 value type system. |
| func simplifyImpliedValueType(ty cty.Type) cty.Type { |
| switch { |
| case ty.IsTupleType(): |
| // If all of the element types are the same then we'll make this |
| // a list instead. This is very likely to be true, since prior versions |
| // of Terraform did not officially support mixed-type collections. |
| |
| if ty.Equals(cty.EmptyTuple) { |
| // Don't know what the element type would be, then. |
| return ty |
| } |
| |
| etys := ty.TupleElementTypes() |
| ety := etys[0] |
| for _, other := range etys[1:] { |
| if !other.Equals(ety) { |
| // inconsistent types |
| return ty |
| } |
| } |
| ety = simplifyImpliedValueType(ety) |
| return cty.List(ety) |
| |
| case ty.IsObjectType(): |
| // If all of the attribute types are the same then we'll make this |
| // a map instead. This is very likely to be true, since prior versions |
| // of Terraform did not officially support mixed-type collections. |
| |
| if ty.Equals(cty.EmptyObject) { |
| // Don't know what the element type would be, then. |
| return ty |
| } |
| |
| atys := ty.AttributeTypes() |
| var ety cty.Type |
| for _, other := range atys { |
| if ety == cty.NilType { |
| ety = other |
| continue |
| } |
| if !other.Equals(ety) { |
| // inconsistent types |
| return ty |
| } |
| } |
| ety = simplifyImpliedValueType(ety) |
| return cty.Map(ety) |
| |
| default: |
| // No other normalizations are possible |
| return ty |
| } |
| } |