github.com/opentofu/opentofu@v1.7.1/internal/states/statefile/read.go (about)

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