github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/states/statefile/read.go (about)

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