| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package workdir |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| |
| "github.com/zclconf/go-cty/cty" |
| ctyjson "github.com/zclconf/go-cty/cty/json" |
| |
| "github.com/hashicorp/terraform/internal/configs/configschema" |
| "github.com/hashicorp/terraform/internal/plans" |
| "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 |
| // configuration. |
| Backend *BackendState `json:"backend,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 [BackendState.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") |
| } |
| |
| return &stateFile, nil |
| } |
| |
| func EncodeBackendStateFile(f *BackendStateFile) ([]byte, error) { |
| f.Version = 3 // we only support version 3 |
| f.TFVersion = version.SemVer.String() |
| 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(), |
| } |
| 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 |
| } |
| |
| // BackendState describes the physical storage format for the backend state |
| // in a working directory, and provides the lowest-level API for decoding it. |
| type BackendState struct { |
| Type string `json:"type"` // Backend type |
| ConfigRaw json.RawMessage `json:"config"` // Backend raw config |
| Hash uint64 `json:"hash"` // Hash of portion of configuration from config files |
| } |
| |
| // Empty returns true if there is no active backend. |
| // |
| // In practice this typically means that the working directory is using the |
| // implied local backend, but that decision is made by the caller. |
| func (s *BackendState) Empty() bool { |
| return s == nil || s.Type == "" |
| } |
| |
| // Config decodes the type-specific configuration object using the provided |
| // schema and returns the result as a cty.Value. |
| // |
| // An error is returned if the stored configuration does not conform to the |
| // given schema, or is otherwise invalid. |
| func (s *BackendState) Config(schema *configschema.Block) (cty.Value, error) { |
| ty := schema.ImpliedType() |
| if s == nil { |
| return cty.NullVal(ty), nil |
| } |
| return ctyjson.Unmarshal(s.ConfigRaw, ty) |
| } |
| |
| // SetConfig replaces (in-place) the type-specific configuration object using |
| // the provided value and associated schema. |
| // |
| // An error is returned if the given value does not conform to the implied |
| // type of the schema. |
| func (s *BackendState) SetConfig(val cty.Value, schema *configschema.Block) error { |
| ty := schema.ImpliedType() |
| buf, err := ctyjson.Marshal(val, ty) |
| if err != nil { |
| return err |
| } |
| s.ConfigRaw = buf |
| return nil |
| } |
| |
| // ForPlan produces an alternative representation of the reciever that is |
| // suitable for storing in a plan. The current workspace must additionally |
| // be provided, to be stored alongside the backend configuration. |
| // |
| // The backend configuration schema is required in order to properly |
| // encode the backend-specific configuration settings. |
| func (s *BackendState) ForPlan(schema *configschema.Block, workspaceName string) (*plans.Backend, error) { |
| if s == nil { |
| return nil, nil |
| } |
| |
| configVal, err := s.Config(schema) |
| if err != nil { |
| return nil, fmt.Errorf("failed to decode backend config: %w", err) |
| } |
| return plans.NewBackend(s.Type, configVal, schema, workspaceName) |
| } |
| |
| func (s *BackendState) DeepCopy() *BackendState { |
| if s == nil { |
| return nil |
| } |
| ret := &BackendState{ |
| Type: s.Type, |
| Hash: s.Hash, |
| } |
| |
| if s.ConfigRaw != nil { |
| ret.ConfigRaw = make([]byte, len(s.ConfigRaw)) |
| copy(ret.ConfigRaw, s.ConfigRaw) |
| } |
| return ret |
| } |