github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/states/statefile/read.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package statefile
     5  
     6  import (
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"os"
    13  
    14  	version "github.com/hashicorp/go-version"
    15  
    16  	"github.com/terramate-io/tf/tfdiags"
    17  	tfversion "github.com/terramate-io/tf/version"
    18  )
    19  
    20  // ErrNoState is returned by ReadState when the state file is empty.
    21  var ErrNoState = errors.New("no state")
    22  
    23  // ErrUnusableState is an error wrapper to indicate that we *think* the input
    24  // represents state data, but can't use it for some reason (as explained in the
    25  // error text). Callers can check against this type with errors.As() if they
    26  // need to distinguish between corrupt state and more fundamental problems like
    27  // an empty file.
    28  type ErrUnusableState struct {
    29  	inner error
    30  }
    31  
    32  func errUnusable(err error) *ErrUnusableState {
    33  	return &ErrUnusableState{inner: err}
    34  }
    35  func (e *ErrUnusableState) Error() string {
    36  	return e.inner.Error()
    37  }
    38  func (e *ErrUnusableState) Unwrap() error {
    39  	return e.inner
    40  }
    41  
    42  // Read reads a state from the given reader.
    43  //
    44  // Legacy state format versions 1 through 3 are supported, but the result will
    45  // contain object attributes in the deprecated "flatmap" format and so must
    46  // be upgraded by the caller before use.
    47  //
    48  // If the state file is empty, the special error value ErrNoState is returned.
    49  // Otherwise, the returned error might be a wrapper around tfdiags.Diagnostics
    50  // potentially describing multiple errors.
    51  func Read(r io.Reader) (*File, error) {
    52  	// Some callers provide us a "typed nil" *os.File here, which would
    53  	// cause us to panic below if we tried to use it.
    54  	if f, ok := r.(*os.File); ok && f == nil {
    55  		return nil, ErrNoState
    56  	}
    57  
    58  	var diags tfdiags.Diagnostics
    59  
    60  	// We actually just buffer the whole thing in memory, because states are
    61  	// generally not huge and we need to do be able to sniff for a version
    62  	// number before full parsing.
    63  	src, err := ioutil.ReadAll(r)
    64  	if err != nil {
    65  		diags = diags.Append(tfdiags.Sourceless(
    66  			tfdiags.Error,
    67  			"Failed to read state file",
    68  			fmt.Sprintf("The state file could not be read: %s", err),
    69  		))
    70  		return nil, diags.Err()
    71  	}
    72  
    73  	if len(src) == 0 {
    74  		return nil, ErrNoState
    75  	}
    76  
    77  	state, err := readState(src)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	if state == nil {
    83  		// Should never happen
    84  		panic("readState returned nil state with no errors")
    85  	}
    86  
    87  	return state, diags.Err()
    88  }
    89  
    90  func readState(src []byte) (*File, error) {
    91  	var diags tfdiags.Diagnostics
    92  
    93  	if looksLikeVersion0(src) {
    94  		diags = diags.Append(tfdiags.Sourceless(
    95  			tfdiags.Error,
    96  			unsupportedFormat,
    97  			"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.",
    98  		))
    99  		return nil, errUnusable(diags.Err())
   100  	}
   101  
   102  	version, versionDiags := sniffJSONStateVersion(src)
   103  	diags = diags.Append(versionDiags)
   104  	if versionDiags.HasErrors() {
   105  		// This is the last point where there's a really good chance it's not a
   106  		// state file at all. Past here, we'll assume errors mean it's state but
   107  		// we can't use it.
   108  		return nil, diags.Err()
   109  	}
   110  
   111  	var result *File
   112  	var err error
   113  	switch version {
   114  	case 0:
   115  		diags = diags.Append(tfdiags.Sourceless(
   116  			tfdiags.Error,
   117  			unsupportedFormat,
   118  			"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.",
   119  		))
   120  	case 1:
   121  		result, diags = readStateV1(src)
   122  	case 2:
   123  		result, diags = readStateV2(src)
   124  	case 3:
   125  		result, diags = readStateV3(src)
   126  	case 4:
   127  		result, diags = readStateV4(src)
   128  	default:
   129  		thisVersion := tfversion.SemVer.String()
   130  		creatingVersion := sniffJSONStateTerraformVersion(src)
   131  		switch {
   132  		case creatingVersion != "":
   133  			diags = diags.Append(tfdiags.Sourceless(
   134  				tfdiags.Error,
   135  				unsupportedFormat,
   136  				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),
   137  			))
   138  		default:
   139  			diags = diags.Append(tfdiags.Sourceless(
   140  				tfdiags.Error,
   141  				unsupportedFormat,
   142  				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),
   143  			))
   144  		}
   145  	}
   146  
   147  	if diags.HasErrors() {
   148  		err = errUnusable(diags.Err())
   149  	}
   150  
   151  	return result, err
   152  }
   153  
   154  func sniffJSONStateVersion(src []byte) (uint64, tfdiags.Diagnostics) {
   155  	var diags tfdiags.Diagnostics
   156  
   157  	type VersionSniff struct {
   158  		Version *uint64 `json:"version"`
   159  	}
   160  	var sniff VersionSniff
   161  	err := json.Unmarshal(src, &sniff)
   162  	if err != nil {
   163  		switch tErr := err.(type) {
   164  		case *json.SyntaxError:
   165  			diags = diags.Append(tfdiags.Sourceless(
   166  				tfdiags.Error,
   167  				unsupportedFormat,
   168  				fmt.Sprintf("The state file could not be parsed as JSON: syntax error at byte offset %d.", tErr.Offset),
   169  			))
   170  		case *json.UnmarshalTypeError:
   171  			diags = diags.Append(tfdiags.Sourceless(
   172  				tfdiags.Error,
   173  				unsupportedFormat,
   174  				fmt.Sprintf("The version in the state file is %s. A positive whole number is required.", tErr.Value),
   175  			))
   176  		default:
   177  			diags = diags.Append(tfdiags.Sourceless(
   178  				tfdiags.Error,
   179  				unsupportedFormat,
   180  				"The state file could not be parsed as JSON.",
   181  			))
   182  		}
   183  	}
   184  
   185  	if sniff.Version == nil {
   186  		diags = diags.Append(tfdiags.Sourceless(
   187  			tfdiags.Error,
   188  			unsupportedFormat,
   189  			"The state file does not have a \"version\" attribute, which is required to identify the format version.",
   190  		))
   191  		return 0, diags
   192  	}
   193  
   194  	return *sniff.Version, diags
   195  }
   196  
   197  // sniffJSONStateTerraformVersion attempts to sniff the Terraform version
   198  // specification from the given state file source code. The result is either
   199  // a version string or an empty string if no version number could be extracted.
   200  //
   201  // This is a best-effort function intended to produce nicer error messages. It
   202  // should not be used for any real processing.
   203  func sniffJSONStateTerraformVersion(src []byte) string {
   204  	type VersionSniff struct {
   205  		Version string `json:"terraform_version"`
   206  	}
   207  	var sniff VersionSniff
   208  
   209  	err := json.Unmarshal(src, &sniff)
   210  	if err != nil {
   211  		return ""
   212  	}
   213  
   214  	// Attempt to parse the string as a version so we won't report garbage
   215  	// as a version number.
   216  	_, err = version.NewVersion(sniff.Version)
   217  	if err != nil {
   218  		return ""
   219  	}
   220  
   221  	return sniff.Version
   222  }
   223  
   224  // unsupportedFormat is a diagnostic summary message for when the state file
   225  // seems to not be a state file at all, or is not a supported version.
   226  //
   227  // Use invalidFormat instead for the subtly-different case of "this looks like
   228  // it's intended to be a state file but it's not structured correctly".
   229  const unsupportedFormat = "Unsupported state file format"
   230  
   231  const upgradeFailed = "State format upgrade failed"