// 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") } 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 }