| package statefile |
| |
| import ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| |
| version "github.com/hashicorp/go-version" |
| |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| tfversion "github.com/hashicorp/terraform/version" |
| ) |
| |
| // ErrNoState is returned by ReadState when the state file is empty. |
| var ErrNoState = errors.New("no state") |
| |
| // Read reads a state from the given reader. |
| // |
| // Legacy state format versions 1 through 3 are supported, but the result will |
| // contain object attributes in the deprecated "flatmap" format and so must |
| // be upgraded by the caller before use. |
| // |
| // If the state file is empty, the special error value ErrNoState is returned. |
| // Otherwise, the returned error might be a wrapper around tfdiags.Diagnostics |
| // potentially describing multiple errors. |
| func Read(r io.Reader) (*File, error) { |
| // Some callers provide us a "typed nil" *os.File here, which would |
| // cause us to panic below if we tried to use it. |
| if f, ok := r.(*os.File); ok && f == nil { |
| return nil, ErrNoState |
| } |
| |
| var diags tfdiags.Diagnostics |
| |
| // We actually just buffer the whole thing in memory, because states are |
| // generally not huge and we need to do be able to sniff for a version |
| // number before full parsing. |
| src, err := ioutil.ReadAll(r) |
| if err != nil { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Failed to read state file", |
| fmt.Sprintf("The state file could not be read: %s", err), |
| )) |
| return nil, diags.Err() |
| } |
| |
| if len(src) == 0 { |
| return nil, ErrNoState |
| } |
| |
| state, diags := readState(src) |
| if diags.HasErrors() { |
| return nil, diags.Err() |
| } |
| |
| if state == nil { |
| // Should never happen |
| panic("readState returned nil state with no errors") |
| } |
| |
| return state, diags.Err() |
| } |
| |
| func readState(src []byte) (*File, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| |
| if looksLikeVersion0(src) { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| unsupportedFormat, |
| "The state is stored in a legacy binary format that is not supported since Terraform v0.7. To continue, first upgrade the state using Terraform 0.6.16 or earlier.", |
| )) |
| return nil, diags |
| } |
| |
| version, versionDiags := sniffJSONStateVersion(src) |
| diags = diags.Append(versionDiags) |
| if versionDiags.HasErrors() { |
| return nil, diags |
| } |
| |
| switch version { |
| case 0: |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| unsupportedFormat, |
| "The state file uses JSON syntax but has a version number of zero. There was never a JSON-based state format zero, so this state file is invalid and cannot be processed.", |
| )) |
| return nil, diags |
| case 1: |
| return readStateV1(src) |
| case 2: |
| return readStateV2(src) |
| case 3: |
| return readStateV3(src) |
| case 4: |
| return readStateV4(src) |
| default: |
| thisVersion := tfversion.SemVer.String() |
| creatingVersion := sniffJSONStateTerraformVersion(src) |
| switch { |
| case creatingVersion != "": |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| unsupportedFormat, |
| fmt.Sprintf("The state file uses format version %d, which is not supported by Terraform %s. This state file was created by Terraform %s.", version, thisVersion, creatingVersion), |
| )) |
| default: |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| unsupportedFormat, |
| fmt.Sprintf("The state file uses format version %d, which is not supported by Terraform %s. This state file may have been created by a newer version of Terraform.", version, thisVersion), |
| )) |
| } |
| return nil, diags |
| } |
| } |
| |
| func sniffJSONStateVersion(src []byte) (uint64, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| |
| type VersionSniff struct { |
| Version *uint64 `json:"version"` |
| } |
| var sniff VersionSniff |
| err := json.Unmarshal(src, &sniff) |
| if err != nil { |
| switch tErr := err.(type) { |
| case *json.SyntaxError: |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| unsupportedFormat, |
| fmt.Sprintf("The state file could not be parsed as JSON: syntax error at byte offset %d.", tErr.Offset), |
| )) |
| case *json.UnmarshalTypeError: |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| unsupportedFormat, |
| fmt.Sprintf("The version in the state file is %s. A positive whole number is required.", tErr.Value), |
| )) |
| default: |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| unsupportedFormat, |
| "The state file could not be parsed as JSON.", |
| )) |
| } |
| } |
| |
| if sniff.Version == nil { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| unsupportedFormat, |
| "The state file does not have a \"version\" attribute, which is required to identify the format version.", |
| )) |
| return 0, diags |
| } |
| |
| return *sniff.Version, diags |
| } |
| |
| // sniffJSONStateTerraformVersion attempts to sniff the Terraform version |
| // specification from the given state file source code. The result is either |
| // a version string or an empty string if no version number could be extracted. |
| // |
| // This is a best-effort function intended to produce nicer error messages. It |
| // should not be used for any real processing. |
| func sniffJSONStateTerraformVersion(src []byte) string { |
| type VersionSniff struct { |
| Version string `json:"terraform_version"` |
| } |
| var sniff VersionSniff |
| |
| err := json.Unmarshal(src, &sniff) |
| if err != nil { |
| return "" |
| } |
| |
| // Attempt to parse the string as a version so we won't report garbage |
| // as a version number. |
| _, err = version.NewVersion(sniff.Version) |
| if err != nil { |
| return "" |
| } |
| |
| return sniff.Version |
| } |
| |
| // unsupportedFormat is a diagnostic summary message for when the state file |
| // seems to not be a state file at all, or is not a supported version. |
| // |
| // Use invalidFormat instead for the subtly-different case of "this looks like |
| // it's intended to be a state file but it's not structured correctly". |
| const unsupportedFormat = "Unsupported state file format" |
| |
| const upgradeFailed = "State format upgrade failed" |