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"