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"