mirror of
https://github.com/hashicorp/terraform.git
synced 2026-03-21 18:10:30 -04:00
158 lines
6.2 KiB
Go
158 lines
6.2 KiB
Go
// 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
|
|
}
|