github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/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-plugin-sdk/internal/tfdiags"
    14  	tfversion "github.com/hashicorp/terraform-plugin-sdk/internal/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  	if state.TerraformVersion != nil && state.TerraformVersion.GreaterThan(tfversion.SemVer) {
    66  		return state, fmt.Errorf(
    67  			"state snapshot was created by Terraform v%s, which is newer than current v%s; upgrade to Terraform v%s or greater to work with this state",
    68  			state.TerraformVersion,
    69  			tfversion.SemVer,
    70  			state.TerraformVersion,
    71  		)
    72  	}
    73  
    74  	return state, diags.Err()
    75  }
    76  
    77  func readState(src []byte) (*File, tfdiags.Diagnostics) {
    78  	var diags tfdiags.Diagnostics
    79  
    80  	if looksLikeVersion0(src) {
    81  		diags = diags.Append(tfdiags.Sourceless(
    82  			tfdiags.Error,
    83  			unsupportedFormat,
    84  			"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.",
    85  		))
    86  		return nil, diags
    87  	}
    88  
    89  	version, versionDiags := sniffJSONStateVersion(src)
    90  	diags = diags.Append(versionDiags)
    91  	if versionDiags.HasErrors() {
    92  		return nil, diags
    93  	}
    94  
    95  	switch version {
    96  	case 0:
    97  		diags = diags.Append(tfdiags.Sourceless(
    98  			tfdiags.Error,
    99  			unsupportedFormat,
   100  			"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.",
   101  		))
   102  		return nil, diags
   103  	case 1:
   104  		return readStateV1(src)
   105  	case 2:
   106  		return readStateV2(src)
   107  	case 3:
   108  		return readStateV3(src)
   109  	case 4:
   110  		return readStateV4(src)
   111  	default:
   112  		thisVersion := tfversion.SemVer.String()
   113  		creatingVersion := sniffJSONStateTerraformVersion(src)
   114  		switch {
   115  		case creatingVersion != "":
   116  			diags = diags.Append(tfdiags.Sourceless(
   117  				tfdiags.Error,
   118  				unsupportedFormat,
   119  				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),
   120  			))
   121  		default:
   122  			diags = diags.Append(tfdiags.Sourceless(
   123  				tfdiags.Error,
   124  				unsupportedFormat,
   125  				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),
   126  			))
   127  		}
   128  		return nil, diags
   129  	}
   130  }
   131  
   132  func sniffJSONStateVersion(src []byte) (uint64, tfdiags.Diagnostics) {
   133  	var diags tfdiags.Diagnostics
   134  
   135  	type VersionSniff struct {
   136  		Version *uint64 `json:"version"`
   137  	}
   138  	var sniff VersionSniff
   139  	err := json.Unmarshal(src, &sniff)
   140  	if err != nil {
   141  		switch tErr := err.(type) {
   142  		case *json.SyntaxError:
   143  			diags = diags.Append(tfdiags.Sourceless(
   144  				tfdiags.Error,
   145  				unsupportedFormat,
   146  				fmt.Sprintf("The state file could not be parsed as JSON: syntax error at byte offset %d.", tErr.Offset),
   147  			))
   148  		case *json.UnmarshalTypeError:
   149  			diags = diags.Append(tfdiags.Sourceless(
   150  				tfdiags.Error,
   151  				unsupportedFormat,
   152  				fmt.Sprintf("The version in the state file is %s. A positive whole number is required.", tErr.Value),
   153  			))
   154  		default:
   155  			diags = diags.Append(tfdiags.Sourceless(
   156  				tfdiags.Error,
   157  				unsupportedFormat,
   158  				"The state file could not be parsed as JSON.",
   159  			))
   160  		}
   161  	}
   162  
   163  	if sniff.Version == nil {
   164  		diags = diags.Append(tfdiags.Sourceless(
   165  			tfdiags.Error,
   166  			unsupportedFormat,
   167  			"The state file does not have a \"version\" attribute, which is required to identify the format version.",
   168  		))
   169  		return 0, diags
   170  	}
   171  
   172  	return *sniff.Version, diags
   173  }
   174  
   175  // sniffJSONStateTerraformVersion attempts to sniff the Terraform version
   176  // specification from the given state file source code. The result is either
   177  // a version string or an empty string if no version number could be extracted.
   178  //
   179  // This is a best-effort function intended to produce nicer error messages. It
   180  // should not be used for any real processing.
   181  func sniffJSONStateTerraformVersion(src []byte) string {
   182  	type VersionSniff struct {
   183  		Version string `json:"terraform_version"`
   184  	}
   185  	var sniff VersionSniff
   186  
   187  	err := json.Unmarshal(src, &sniff)
   188  	if err != nil {
   189  		return ""
   190  	}
   191  
   192  	// Attempt to parse the string as a version so we won't report garbage
   193  	// as a version number.
   194  	_, err = version.NewVersion(sniff.Version)
   195  	if err != nil {
   196  		return ""
   197  	}
   198  
   199  	return sniff.Version
   200  }
   201  
   202  // unsupportedFormat is a diagnostic summary message for when the state file
   203  // seems to not be a state file at all, or is not a supported version.
   204  //
   205  // Use invalidFormat instead for the subtly-different case of "this looks like
   206  // it's intended to be a state file but it's not structured correctly".
   207  const unsupportedFormat = "Unsupported state file format"
   208  
   209  const upgradeFailed = "State format upgrade failed"