| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package workdir | |
| import ( | |
| "encoding/json" | |
| "fmt" | |
| "github.com/hashicorp/terraform/version" | |
| ) | |
| // BackendStateFile describes the overall structure of the file format used | |
| // to track a working directory's active backend. | |
| // | |
| // The main interesting part of this is the [BackendStateFile.Backend] field, | |
| // but [BackendStateFile.Version] is also important to make sure that the | |
| // current Terraform CLI version will be able to understand the file. | |
| type BackendStateFile struct { | |
| // Don't access this directly. It's here only for use during serialization | |
| // and deserialization of backend state file contents. | |
| Version int `json:"version"` | |
| // TFVersion is the version of Terraform that wrote this state. This is | |
| // really just for debugging purposes; we don't currently vary behavior | |
| // based on this field. | |
| TFVersion string `json:"terraform_version,omitempty"` | |
| // Backend tracks the configuration for the backend in use with | |
| // this state. This is used to track any changes in the `backend` | |
| // block's configuration. | |
| // Note: this also used to tracking changes in the `cloud` block | |
| Backend *BackendConfigState `json:"backend,omitempty"` | |
| // StateStore tracks the configuration for a state store in use | |
| // with this state. This is used to track any changes in the `state_store` | |
| // block's configuration or associated data about the provider facilitating | |
| // state storage | |
| StateStore *StateStoreConfigState `json:"state_store,omitempty"` | |
| // This is here just so we can sniff for the unlikely-but-possible | |
| // situation that someone is trying to use modern Terraform with a | |
| // directory that was most recently used with Terraform v0.8, before | |
| // there was any concept of backends. Don't access this field. | |
| Remote *struct{} `json:"remote,omitempty"` | |
| } | |
| // NewBackendStateFile returns a new [BackendStateFile] object that initially | |
| // has no backend configured. | |
| // | |
| // Callers should then mutate [BackendStateFile.Backend] in the result to | |
| // specify the explicit backend in use, if any. | |
| func NewBackendStateFile() *BackendStateFile { | |
| return &BackendStateFile{ | |
| // NOTE: We don't populate Version or TFVersion here because we | |
| // always clobber those when encoding a state file in | |
| // [EncodeBackendStateFile]. | |
| } | |
| } | |
| // ParseBackendStateFile tries to decode the given byte slice as the backend | |
| // state file format. | |
| // | |
| // Returns an error if the content is not valid syntax, or if the file is | |
| // of an unsupported format version. | |
| // | |
| // This does not immediately decode the embedded backend config, and so | |
| // it's possible that a subsequent call to [BackendConfigState.Config] will | |
| // return further errors even if this call succeeds. | |
| func ParseBackendStateFile(src []byte) (*BackendStateFile, error) { | |
| // To avoid any weird collisions with as-yet-unknown future versions of | |
| // the format, we'll do a first pass of decoding just the "version" | |
| // property, and then decode the rest only if we find the version number | |
| // that we're expecting. | |
| type VersionSniff struct { | |
| Version int `json:"version"` | |
| TFVersion string `json:"terraform_version,omitempty"` | |
| } | |
| var versionSniff VersionSniff | |
| err := json.Unmarshal(src, &versionSniff) | |
| if err != nil { | |
| return nil, fmt.Errorf("invalid syntax: %w", err) | |
| } | |
| if versionSniff.Version == 0 { | |
| // This could either mean that it's explicitly "version": 0 or that | |
| // the version property is missing. We'll assume the latter here | |
| // because state snapshot version 0 was an encoding/gob binary format | |
| // rather than a JSON format and so it would be very weird for | |
| // that to show up in a JSON file. | |
| return nil, fmt.Errorf("invalid syntax: no format version number") | |
| } | |
| if versionSniff.Version != 3 { | |
| return nil, fmt.Errorf("unsupported backend state version %d; you may need to use Terraform CLI v%s to work in this directory", versionSniff.Version, versionSniff.TFVersion) | |
| } | |
| // If we get here then we can be sure that this file at least _thinks_ | |
| // it's format version 3. | |
| var stateFile BackendStateFile | |
| err = json.Unmarshal(src, &stateFile) | |
| if err != nil { | |
| return nil, fmt.Errorf("invalid syntax: %w", err) | |
| } | |
| if stateFile.Backend == nil && stateFile.Remote != nil { | |
| // It's very unlikely to get here, but one way it could happen is | |
| // if this working directory was most recently used with Terraform v0.8 | |
| // or earlier, which didn't yet include the concept of backends. | |
| // This error message assumes that's the case. | |
| return nil, fmt.Errorf("this working directory uses legacy remote state and so must first be upgraded using Terraform v0.9") | |
| } | |
| if stateFile.Backend != nil && stateFile.StateStore != nil { | |
| return nil, fmt.Errorf("encountered a malformed backend state file that contains state for both a 'backend' and a 'state_store' block") | |
| } | |
| if stateFile.StateStore != nil && stateFile.StateStore.ProviderSupplyMode == "" { | |
| // Check for this, as lacking this data can cause problems later when an empty provider version | |
| // is encountered. This error will make debugging much easier. | |
| return nil, fmt.Errorf("encountered a malformed backend state file with a 'state_store' block that is missing the required 'provider_supply_mode' property") | |
| } | |
| return &stateFile, nil | |
| } | |
| func EncodeBackendStateFile(f *BackendStateFile) ([]byte, error) { | |
| f.Version = 3 // we only support version 3 | |
| f.TFVersion = version.SemVer.String() | |
| switch { | |
| case f.Backend != nil && f.StateStore != nil: | |
| return nil, fmt.Errorf("attempted to encode a malformed backend state file; it contains state for both a 'backend' and a 'state_store' block. This is a bug in Terraform and should be reported.") | |
| case f.Backend == nil && f.StateStore == nil: | |
| // This is valid - if the user has a backend state file and an implied local backend in use | |
| // the backend state file exists but has no Backend data. | |
| case f.Backend != nil: | |
| // Not implementing anything here - risk of breaking changes | |
| case f.StateStore != nil: | |
| err := f.StateStore.Validate() | |
| if err != nil { | |
| return nil, err | |
| } | |
| default: | |
| panic("error when determining whether backend state file was valid. This is a bug in Terraform and should be reported.") | |
| } | |
| return json.MarshalIndent(f, "", " ") | |
| } | |
| func (f *BackendStateFile) DeepCopy() *BackendStateFile { | |
| if f == nil { | |
| return nil | |
| } | |
| ret := &BackendStateFile{ | |
| Version: f.Version, | |
| TFVersion: f.TFVersion, | |
| Backend: f.Backend.DeepCopy(), | |
| StateStore: f.StateStore.DeepCopy(), | |
| } | |
| if f.Remote != nil { | |
| // This shouldn't ever be present in an object held by a caller since | |
| // we'd return an error about it during load, but we'll set it anyway | |
| // just to minimize surprise. | |
| ret.Remote = &struct{}{} | |
| } | |
| return ret | |
| } |