You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
terraform/internal/command/workdir/statestore_config_state.go

222 lines
8.8 KiB

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package workdir
import (
"encoding/json"
"errors"
"fmt"
version "github.com/hashicorp/go-version"
tfaddr "github.com/hashicorp/terraform-registry-address"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/plans"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
)
var (
_ ConfigState = &StateStoreConfigState{}
_ DeepCopier[StateStoreConfigState] = &StateStoreConfigState{}
_ PlanDataProvider[plans.StateStore] = &StateStoreConfigState{}
)
// StateStoreConfigState describes the physical storage format for the state store
type StateStoreConfigState struct {
Type string `json:"type"` // State store type name
Provider *ProviderConfigState `json:"provider"` // Details about the state-storage provider
ConfigRaw json.RawMessage `json:"config"` // state_store block raw config, barring provider details
Hash uint64 `json:"hash"` // Hash of the state_store block's configuration, including the nested provider block
ProviderSupplyMode getproviders.ProviderSupplyMode `json:"provider_supply_mode"` // How the provider was supplied to Terraform during the init operation that created this config state.
}
// Empty returns true if there is no active state store.
func (s *StateStoreConfigState) Empty() bool {
return s == nil || s.Type == ""
}
// Validate returns true if there are no missing expected values, and
// important values have been validated, e.g. FQNs. When the config is
// invalid an error will be returned.
func (s *StateStoreConfigState) Validate() error {
// Are any bits of data totally missing?
if s.Empty() {
return fmt.Errorf("attempted to encode a malformed backend state file; data is empty")
}
if s.Type == "" {
return fmt.Errorf("attempted to encode a malformed backend state file; state store type is missing")
}
if s.Provider == nil {
return fmt.Errorf("attempted to encode a malformed backend state file; provider data is missing")
}
if s.ConfigRaw == nil {
return fmt.Errorf("attempted to encode a malformed backend state file; state_store configuration data is missing")
}
// Validity of data that is there
err := s.Provider.Source.Validate()
if err != nil {
return fmt.Errorf("state store is not valid: %w", err)
}
// Version information is required if the provider isn't builtin or unmanaged by Terraform
switch s.ProviderSupplyMode {
case getproviders.BuiltIn, getproviders.Reattached, getproviders.DevOverride:
// These modes do not require version information
case getproviders.ManagedByTerraform:
if s.Provider.Version == nil {
return fmt.Errorf("state store is not valid: provider version data is missing despite provider %s being managed by Terraform.", s.Provider.Source.ForDisplay())
}
default:
panic(fmt.Sprintf("State store provider %q (%s) has unknown supply mode %q. This is a bug in Terraform and should be reported.", s.Provider.Source.Type, s.Provider.Source.ForDisplay(), s.ProviderSupplyMode))
}
return nil
}
// 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 *StateStoreConfigState) 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 *StateStoreConfigState) SetConfig(val cty.Value, schema *configschema.Block) error {
if s == nil {
return errors.New("SetConfig called on nil StateStoreConfigState receiver")
}
ty := schema.ImpliedType()
buf, err := ctyjson.Marshal(val, ty)
if err != nil {
return err
}
s.ConfigRaw = buf
return nil
}
// PlanData produces an alternative representation of the receiver that is
// suitable for storing in a plan. The current workspace must additionally
// be provided, to be stored alongside the state store configuration.
//
// The state_store configuration schema is required in order to properly
// encode the state store-specific configuration settings.
func (s *StateStoreConfigState) PlanData(storeSchema *configschema.Block, providerSchema *configschema.Block, workspaceName string) (*plans.StateStore, error) {
if s == nil {
panic("PlanData called on a nil *StateStoreConfigState receiver. This is a bug in Terraform and should be reported.")
}
if err := s.Validate(); err != nil {
return nil, fmt.Errorf("error when preparing state store config for planfile: %s", err)
}
storeConfigVal, err := s.Config(storeSchema)
if err != nil {
return nil, fmt.Errorf("failed to decode state_store config: %w", err)
}
providerConfigVal, err := s.Provider.Config(providerSchema)
if err != nil {
return nil, fmt.Errorf("failed to decode state_store's nested provider config: %w", err)
}
var providerVersion *version.Version
switch s.ProviderSupplyMode {
case getproviders.BuiltIn, getproviders.Reattached, getproviders.DevOverride:
// For built-in providers, reattached providers, and developer overrides, we don't require version information to be present in the state file, so we should be tolerant of it being missing.
// In this case we can just use a placeholder version that will never actually be used for anything, but allows us to avoid returning an error when trying to save state store data to a plan file.
providerVersion = version.Must(version.NewVersion("0.0.0"))
case getproviders.ManagedByTerraform:
providerVersion = s.Provider.Version
default:
panic(fmt.Sprintf("State store provider %q (%s) has unknown supply mode %q. This is a bug in Terraform and should be reported.", s.Provider.Source.Type, s.Provider.Source.ForDisplay(), s.ProviderSupplyMode))
}
return plans.NewStateStore(s.Type, providerVersion, s.Provider.Source, storeConfigVal, storeSchema, providerConfigVal, providerSchema, workspaceName)
}
func (s *StateStoreConfigState) DeepCopy() *StateStoreConfigState {
if s == nil {
return nil
}
provider := &ProviderConfigState{
Version: s.Provider.Version,
Source: s.Provider.Source,
}
if s.Provider.ConfigRaw != nil {
provider.ConfigRaw = make([]byte, len(s.Provider.ConfigRaw))
copy(provider.ConfigRaw, s.Provider.ConfigRaw)
}
ret := &StateStoreConfigState{
Type: s.Type,
Provider: provider,
ProviderSupplyMode: s.ProviderSupplyMode,
Hash: s.Hash,
}
if s.ConfigRaw != nil {
ret.ConfigRaw = make([]byte, len(s.ConfigRaw))
copy(ret.ConfigRaw, s.ConfigRaw)
}
return ret
}
var _ ConfigState = &ProviderConfigState{}
// ProviderConfigState is used in the StateStoreConfigState struct to describe the provider that's used for pluggable
// state storage. The version and source data inside should mirror an entry in the dependency lock file.
// The configuration recorded here is the provider block nested inside state_store block.
type ProviderConfigState struct {
Version *version.Version `json:"version"` // The specific provider version used for the state store. Should be set using a getproviders.Version, etc.
Source *tfaddr.Provider `json:"source"` // The FQN/fully-qualified name of the provider.
ConfigRaw json.RawMessage `json:"config"` // state_store block raw config, barring provider details
}
// Empty returns true if there is no provider config state data.
func (s *ProviderConfigState) Empty() bool {
return s == nil || s.Version == nil || s.ConfigRaw == nil
}
// 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 *ProviderConfigState) 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 *ProviderConfigState) SetConfig(val cty.Value, schema *configschema.Block) error {
if s == nil {
return errors.New("SetConfig called on nil ProviderConfigState receiver")
}
ty := schema.ImpliedType()
buf, err := ctyjson.Marshal(val, ty)
if err != nil {
return err
}
s.ConfigRaw = buf
return nil
}