blob: fb7dbab1f57ae19ecf684b14aaff615a341116d5 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// 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/plans"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
)
var _ ConfigState = &StateStoreConfigState{}
var _ DeepCopier[StateStoreConfigState] = &StateStoreConfigState{}
var _ 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 portion of configuration from config files
}
// 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("state store is not valid: data is empty")
}
if s.Provider == nil {
return fmt.Errorf("state store is not valid: provider data is missing")
}
if s.Provider.Version == nil {
return fmt.Errorf("state store is not valid: version 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)
}
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 {
return nil, nil
}
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)
}
return plans.NewStateStore(s.Type, s.Provider.Version, 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,
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
}