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"