terraform/internal/command/workdir/backend_state.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

159 lines
6.2 KiB
Go
Raw Permalink Normal View History

command/workdir: Model the "backend state" file format Historically (before there were "backends") Terraform had a single state file format used both for real state snapshots and for tracking where remote state was stored. Terraform v0.12 caused these two to be split because we adopted state snapshot version 4 for real snapshots but retained a subset of version 3 for tracking the remote backend configuration in the local working directory. Unfortunately we previously kept that working by retaining a snapshot of the entire Terraform v0.11 "terraform" package as legacy/terraform, which happened to still be around because we also needed to retain a copy of the entire legacy SDK to keep the remote state backends working. This now hoists just the tiny slice of legacy terraform package functionality needed to implement the "backend state" file format into package workdir. This package is a good home for it because it's part of the working directory state. Ideally it would be accessed through methods of the workdir.Dir type, but that's too disruptive a refactor to combine into this and so that'll need to wait for another day; for now we'll keep the existing callers doing their access through our "clistate" package that is itself a forked snapshot of what statemgr.Filesystem used to be in Terraform v0.11. This removes all but one of the uses of "legacy/terraform" aside from calls in the other packages under "legacy". We'll clean up the last one in a later commit, because it's not related to the backend state file format.
2024-03-07 14:58:46 -05:00
// 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
Update backend state file so it can describe PSS state (#37179) * Split code for backend state file vs backend state * Rename BackendState to BackendConfigState * Spelling error * Add `StateStorageConfigState` struct as new implementation of new `ConfigState[T any]` interface. * Split tests for backend state file vs backend config state structs * Rename StateStorageConfigState to StateStoreConfigState * Clarify test name, add comments * Add tests for StateStoreConfigState methods * Add test showing state_store in JSON is parsed correctly * Add detection of malformed backend state files that contain both backend and state_store fields * Add validation that stops a backend state file being written if it will contain state for both backend and state_store blocks * Rename `state_storage` to `state_store` * Rename `state_storage` to `state_store` in filenames * Move`ConfigState` to its own file * Fix test name, remove whitespace * Update `StateStoreConfigState` comment using review suggestion * Update error message to no longer allude to the environment TF is being run in * Update the state_store state to use `version.Version` and an adapted version of `tfaddr.Provider` for marshalling version and source data * Update test helper so it doesn't accidentally supply validation in tests * Add protection against saving an empty backend state file * Remove direct testing of (s *Source) MarshalText() and UnmarshalText() methods * Add Validate method to StateStoreConfigState, use in backend state encoding logic * Refactor to use new features in registry dependency
2025-06-11 10:10:26 -04:00
// 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"`
command/workdir: Model the "backend state" file format Historically (before there were "backends") Terraform had a single state file format used both for real state snapshots and for tracking where remote state was stored. Terraform v0.12 caused these two to be split because we adopted state snapshot version 4 for real snapshots but retained a subset of version 3 for tracking the remote backend configuration in the local working directory. Unfortunately we previously kept that working by retaining a snapshot of the entire Terraform v0.11 "terraform" package as legacy/terraform, which happened to still be around because we also needed to retain a copy of the entire legacy SDK to keep the remote state backends working. This now hoists just the tiny slice of legacy terraform package functionality needed to implement the "backend state" file format into package workdir. This package is a good home for it because it's part of the working directory state. Ideally it would be accessed through methods of the workdir.Dir type, but that's too disruptive a refactor to combine into this and so that'll need to wait for another day; for now we'll keep the existing callers doing their access through our "clistate" package that is itself a forked snapshot of what statemgr.Filesystem used to be in Terraform v0.11. This removes all but one of the uses of "legacy/terraform" aside from calls in the other packages under "legacy". We'll clean up the last one in a later commit, because it's not related to the backend state file format.
2024-03-07 14:58:46 -05:00
// 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
Update backend state file so it can describe PSS state (#37179) * Split code for backend state file vs backend state * Rename BackendState to BackendConfigState * Spelling error * Add `StateStorageConfigState` struct as new implementation of new `ConfigState[T any]` interface. * Split tests for backend state file vs backend config state structs * Rename StateStorageConfigState to StateStoreConfigState * Clarify test name, add comments * Add tests for StateStoreConfigState methods * Add test showing state_store in JSON is parsed correctly * Add detection of malformed backend state files that contain both backend and state_store fields * Add validation that stops a backend state file being written if it will contain state for both backend and state_store blocks * Rename `state_storage` to `state_store` * Rename `state_storage` to `state_store` in filenames * Move`ConfigState` to its own file * Fix test name, remove whitespace * Update `StateStoreConfigState` comment using review suggestion * Update error message to no longer allude to the environment TF is being run in * Update the state_store state to use `version.Version` and an adapted version of `tfaddr.Provider` for marshalling version and source data * Update test helper so it doesn't accidentally supply validation in tests * Add protection against saving an empty backend state file * Remove direct testing of (s *Source) MarshalText() and UnmarshalText() methods * Add Validate method to StateStoreConfigState, use in backend state encoding logic * Refactor to use new features in registry dependency
2025-06-11 10:10:26 -04:00
// it's possible that a subsequent call to [BackendConfigState.Config] will
command/workdir: Model the "backend state" file format Historically (before there were "backends") Terraform had a single state file format used both for real state snapshots and for tracking where remote state was stored. Terraform v0.12 caused these two to be split because we adopted state snapshot version 4 for real snapshots but retained a subset of version 3 for tracking the remote backend configuration in the local working directory. Unfortunately we previously kept that working by retaining a snapshot of the entire Terraform v0.11 "terraform" package as legacy/terraform, which happened to still be around because we also needed to retain a copy of the entire legacy SDK to keep the remote state backends working. This now hoists just the tiny slice of legacy terraform package functionality needed to implement the "backend state" file format into package workdir. This package is a good home for it because it's part of the working directory state. Ideally it would be accessed through methods of the workdir.Dir type, but that's too disruptive a refactor to combine into this and so that'll need to wait for another day; for now we'll keep the existing callers doing their access through our "clistate" package that is itself a forked snapshot of what statemgr.Filesystem used to be in Terraform v0.11. This removes all but one of the uses of "legacy/terraform" aside from calls in the other packages under "legacy". We'll clean up the last one in a later commit, because it's not related to the backend state file format.
2024-03-07 14:58:46 -05:00
// 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")
}
Update backend state file so it can describe PSS state (#37179) * Split code for backend state file vs backend state * Rename BackendState to BackendConfigState * Spelling error * Add `StateStorageConfigState` struct as new implementation of new `ConfigState[T any]` interface. * Split tests for backend state file vs backend config state structs * Rename StateStorageConfigState to StateStoreConfigState * Clarify test name, add comments * Add tests for StateStoreConfigState methods * Add test showing state_store in JSON is parsed correctly * Add detection of malformed backend state files that contain both backend and state_store fields * Add validation that stops a backend state file being written if it will contain state for both backend and state_store blocks * Rename `state_storage` to `state_store` * Rename `state_storage` to `state_store` in filenames * Move`ConfigState` to its own file * Fix test name, remove whitespace * Update `StateStoreConfigState` comment using review suggestion * Update error message to no longer allude to the environment TF is being run in * Update the state_store state to use `version.Version` and an adapted version of `tfaddr.Provider` for marshalling version and source data * Update test helper so it doesn't accidentally supply validation in tests * Add protection against saving an empty backend state file * Remove direct testing of (s *Source) MarshalText() and UnmarshalText() methods * Add Validate method to StateStoreConfigState, use in backend state encoding logic * Refactor to use new features in registry dependency
2025-06-11 10:10:26 -04:00
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")
}
command/workdir: Model the "backend state" file format Historically (before there were "backends") Terraform had a single state file format used both for real state snapshots and for tracking where remote state was stored. Terraform v0.12 caused these two to be split because we adopted state snapshot version 4 for real snapshots but retained a subset of version 3 for tracking the remote backend configuration in the local working directory. Unfortunately we previously kept that working by retaining a snapshot of the entire Terraform v0.11 "terraform" package as legacy/terraform, which happened to still be around because we also needed to retain a copy of the entire legacy SDK to keep the remote state backends working. This now hoists just the tiny slice of legacy terraform package functionality needed to implement the "backend state" file format into package workdir. This package is a good home for it because it's part of the working directory state. Ideally it would be accessed through methods of the workdir.Dir type, but that's too disruptive a refactor to combine into this and so that'll need to wait for another day; for now we'll keep the existing callers doing their access through our "clistate" package that is itself a forked snapshot of what statemgr.Filesystem used to be in Terraform v0.11. This removes all but one of the uses of "legacy/terraform" aside from calls in the other packages under "legacy". We'll clean up the last one in a later commit, because it's not related to the backend state file format.
2024-03-07 14:58:46 -05:00
return &stateFile, nil
}
func EncodeBackendStateFile(f *BackendStateFile) ([]byte, error) {
f.Version = 3 // we only support version 3
f.TFVersion = version.SemVer.String()
Update backend state file so it can describe PSS state (#37179) * Split code for backend state file vs backend state * Rename BackendState to BackendConfigState * Spelling error * Add `StateStorageConfigState` struct as new implementation of new `ConfigState[T any]` interface. * Split tests for backend state file vs backend config state structs * Rename StateStorageConfigState to StateStoreConfigState * Clarify test name, add comments * Add tests for StateStoreConfigState methods * Add test showing state_store in JSON is parsed correctly * Add detection of malformed backend state files that contain both backend and state_store fields * Add validation that stops a backend state file being written if it will contain state for both backend and state_store blocks * Rename `state_storage` to `state_store` * Rename `state_storage` to `state_store` in filenames * Move`ConfigState` to its own file * Fix test name, remove whitespace * Update `StateStoreConfigState` comment using review suggestion * Update error message to no longer allude to the environment TF is being run in * Update the state_store state to use `version.Version` and an adapted version of `tfaddr.Provider` for marshalling version and source data * Update test helper so it doesn't accidentally supply validation in tests * Add protection against saving an empty backend state file * Remove direct testing of (s *Source) MarshalText() and UnmarshalText() methods * Add Validate method to StateStoreConfigState, use in backend state encoding logic * Refactor to use new features in registry dependency
2025-06-11 10:10:26 -04:00
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.")
}
command/workdir: Model the "backend state" file format Historically (before there were "backends") Terraform had a single state file format used both for real state snapshots and for tracking where remote state was stored. Terraform v0.12 caused these two to be split because we adopted state snapshot version 4 for real snapshots but retained a subset of version 3 for tracking the remote backend configuration in the local working directory. Unfortunately we previously kept that working by retaining a snapshot of the entire Terraform v0.11 "terraform" package as legacy/terraform, which happened to still be around because we also needed to retain a copy of the entire legacy SDK to keep the remote state backends working. This now hoists just the tiny slice of legacy terraform package functionality needed to implement the "backend state" file format into package workdir. This package is a good home for it because it's part of the working directory state. Ideally it would be accessed through methods of the workdir.Dir type, but that's too disruptive a refactor to combine into this and so that'll need to wait for another day; for now we'll keep the existing callers doing their access through our "clistate" package that is itself a forked snapshot of what statemgr.Filesystem used to be in Terraform v0.11. This removes all but one of the uses of "legacy/terraform" aside from calls in the other packages under "legacy". We'll clean up the last one in a later commit, because it's not related to the backend state file format.
2024-03-07 14:58:46 -05:00
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(),
command/workdir: Model the "backend state" file format Historically (before there were "backends") Terraform had a single state file format used both for real state snapshots and for tracking where remote state was stored. Terraform v0.12 caused these two to be split because we adopted state snapshot version 4 for real snapshots but retained a subset of version 3 for tracking the remote backend configuration in the local working directory. Unfortunately we previously kept that working by retaining a snapshot of the entire Terraform v0.11 "terraform" package as legacy/terraform, which happened to still be around because we also needed to retain a copy of the entire legacy SDK to keep the remote state backends working. This now hoists just the tiny slice of legacy terraform package functionality needed to implement the "backend state" file format into package workdir. This package is a good home for it because it's part of the working directory state. Ideally it would be accessed through methods of the workdir.Dir type, but that's too disruptive a refactor to combine into this and so that'll need to wait for another day; for now we'll keep the existing callers doing their access through our "clistate" package that is itself a forked snapshot of what statemgr.Filesystem used to be in Terraform v0.11. This removes all but one of the uses of "legacy/terraform" aside from calls in the other packages under "legacy". We'll clean up the last one in a later commit, because it's not related to the backend state file format.
2024-03-07 14:58:46 -05:00
}
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
}