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"