Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/command/meta_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ func (m *Meta) Operation(b backend.Backend, vt arguments.ViewType) *backendrun.O
// here first is a bug, so panic.
panic(fmt.Sprintf("invalid workspace: %s", err))
}
planOutBackend, err := m.backendState.ForPlan(schema, workspace)
planOutBackend, err := m.backendState.PlanData(schema, workspace)
if err != nil {
// Always indicates an implementation error in practice, because
// errors here indicate invalid encoding of the backend configuration
Expand Down
8 changes: 5 additions & 3 deletions internal/command/workdir/backend_config_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import (
"github.com/hashicorp/terraform/internal/plans"
)

var _ ConfigState[BackendConfigState] = &BackendConfigState{}
var _ ConfigState = &BackendConfigState{}
var _ DeepCopier[BackendConfigState] = &BackendConfigState{}
var _ PlanDataProvider[plans.Backend] = &BackendConfigState{}

// BackendConfigState describes the physical storage format for the backend state
// in a working directory, and provides the lowest-level API for decoding it.
Expand Down Expand Up @@ -60,13 +62,13 @@ func (s *BackendConfigState) SetConfig(val cty.Value, schema *configschema.Block
return nil
}

// ForPlan produces an alternative representation of the receiver that is
// 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 backend configuration.
//
// The backend configuration schema is required in order to properly
// encode the backend-specific configuration settings.
func (s *BackendConfigState) ForPlan(schema *configschema.Block, workspaceName string) (*plans.Backend, error) {
func (s *BackendConfigState) PlanData(schema *configschema.Block, workspaceName string) (*plans.Backend, error) {
if s == nil {
return nil, nil
}
Expand Down
19 changes: 14 additions & 5 deletions internal/command/workdir/config_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,24 @@ package workdir

import (
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/plans"
"github.com/zclconf/go-cty/cty"
)

// ConfigState describes a configuration block, and is used to make that config block stateful.
type ConfigState[T any] interface {
type ConfigState interface {
Empty() bool
Config(*configschema.Block) (cty.Value, error)
SetConfig(cty.Value, *configschema.Block) error
ForPlan(*configschema.Block, string) (*plans.Backend, error)
Config(schema *configschema.Block) (cty.Value, error)
SetConfig(val cty.Value, schema *configschema.Block) error
}

// DeepCopier implementations can return deep copies of themselves for use elsewhere
// without mutating the original value.
type DeepCopier[T any] interface {
DeepCopy() *T
}

// PlanDataProvider implementations can return a representation of their data that's
// appropriate for storing in a plan file.
type PlanDataProvider[T any] interface {
PlanData(schema *configschema.Block, workspaceName string) (*T, error)
}
21 changes: 15 additions & 6 deletions internal/command/workdir/statestore_config_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import (
ctyjson "github.com/zclconf/go-cty/cty/json"
)

var _ ConfigState[StateStoreConfigState] = &StateStoreConfigState{}
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 {
Expand Down Expand Up @@ -94,19 +96,26 @@ func (s *StateStoreConfigState) SetConfig(val cty.Value, schema *configschema.Bl
return nil
}

// ForPlan produces an alternative representation of the receiver that is
// 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) ForPlan(schema *configschema.Block, workspaceName string) (*plans.Backend, error) {
func (s *StateStoreConfigState) PlanData(schema *configschema.Block, workspaceName string) (*plans.StateStore, error) {
if s == nil {
return nil, nil
}
// TODO
// What should a pluggable state store look like in a plan?
return nil, nil

if err := s.Validate(); err != nil {
return nil, fmt.Errorf("error when preparing state store config for planfile: %s", err)
}

configVal, err := s.Config(schema)
if err != nil {
return nil, fmt.Errorf("failed to decode state_store config: %w", err)
}
return plans.NewStateStore(s.Type, s.Provider.Version, &s.Provider.Source, configVal, schema, workspaceName)
}

func (s *StateStoreConfigState) DeepCopy() *StateStoreConfigState {
Expand Down
79 changes: 78 additions & 1 deletion internal/plans/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (

"github.com/zclconf/go-cty/cty"

version "github.com/hashicorp/go-version"
tfaddr "github.com/hashicorp/terraform-registry-address"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/collections"
"github.com/hashicorp/terraform/internal/configs/configschema"
Expand Down Expand Up @@ -69,7 +71,9 @@ type Plan struct {
DeferredResources []*DeferredResourceInstanceChangeSrc
TargetAddrs []addrs.Targetable
ForceReplaceAddrs []addrs.AbsResourceInstance
Backend Backend

Backend Backend
StateStore StateStore

// Complete is true if Terraform considers this to be a "complete" plan,
// which is to say that it includes a planned action (even if no-op)
Expand Down Expand Up @@ -228,3 +232,76 @@ func NewBackend(typeName string, config cty.Value, configSchema *configschema.Bl
Workspace: workspaceName,
}, nil
}

// StateStore represents the state store-related configuration and other data as it
// existed when a plan was created.
type StateStore struct {
// Type is the type of state store that the plan will apply against.
Type string

Provider *Provider

// Config is the configuration of the backend, whose schema is decided by
// the backend Type.
Config DynamicValue

// Workspace is the name of the workspace that was active when the plan
// was created. It is illegal to apply a plan created for one workspace
// to the state of another workspace.
// (This constraint is already enforced by the statefile lineage mechanism,
// but storing this explicitly allows us to return a better error message
// in the situation where the user has the wrong workspace selected.)
Workspace string
}

type Provider struct {
Version *version.Version // The specific provider version used for the state store. Should be set using a getproviders.Version, etc.
Source *tfaddr.Provider // The FQN/fully-qualified name of the provider.
}

func NewStateStore(typeName string, ver *version.Version, source *tfaddr.Provider, config cty.Value, configSchema *configschema.Block, workspaceName string) (*StateStore, error) {
dv, err := NewDynamicValue(config, configSchema.ImpliedType())
if err != nil {
return nil, err
}

provider := &Provider{
Version: ver,
Source: source,
}

return &StateStore{
Type: typeName,
Provider: provider,
Config: dv,
Workspace: workspaceName,
}, nil
}

// SetVersion includes logic for parsing a string representation of a version,
// for example data read from a plan file.
// If an error occurs it is returned and the receiver's Version field is unchanged.
// If there are no errors then the receiver's Version field is updated.
func (p *Provider) SetVersion(input string) error {
ver, err := version.NewVersion(input)
if err != nil {
return err
}

p.Version = ver
return nil
}

// SetSource includes logic for parsing a string representation of a provider source,
// for example data read from a plan file.
// If an error occurs it is returned and the receiver's Source field is unchanged.
// If there are no errors then the receiver's Source field is updated.
func (p *Provider) SetSource(input string) error {
source, diags := addrs.ParseProviderSourceString(input)
if diags.HasErrors() {
return diags.ErrWithWarnings()
}

p.Source = &source
return nil
}
6 changes: 4 additions & 2 deletions internal/plans/plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

func TestProviderAddrs(t *testing.T) {

// Prepare plan
plan := &Plan{
VariableValues: map[string]DynamicValue{},
Changes: &ChangesSrc{
Expand Down Expand Up @@ -57,11 +58,12 @@ func TestProviderAddrs(t *testing.T) {

got := plan.ProviderAddrs()
want := []addrs.AbsProviderConfig{
addrs.AbsProviderConfig{
// Providers used for managed resources
{
Module: addrs.RootModule.Child("foo"),
Provider: addrs.NewDefaultProvider("test"),
},
addrs.AbsProviderConfig{
{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
},
Expand Down
72 changes: 60 additions & 12 deletions internal/plans/planfile/tfplan.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,17 @@ func readTfplan(r io.Reader) (*plans.Plan, error) {
)
}

if rawBackend := rawPlan.Backend; rawBackend == nil {
return nil, fmt.Errorf("plan file has no backend settings; backend settings are required")
} else {
switch {
case rawPlan.Backend == nil && rawPlan.StateStore == nil:
// Similar validation in writeTfPlan should prevent this occurring
return nil,
fmt.Errorf("plan file has neither backend nor state_store settings; one of these settings is required. This is a bug in Terraform and should be reported.")
case rawPlan.Backend != nil && rawPlan.StateStore != nil:
// Similar validation in writeTfPlan should prevent this occurring
return nil,
fmt.Errorf("plan file contains both backend and state_store settings when only one of these settings should be set. This is a bug in Terraform and should be reported.")
case rawPlan.Backend != nil:
rawBackend := rawPlan.Backend
config, err := valueFromTfplan(rawBackend.Config)
if err != nil {
return nil, fmt.Errorf("plan file has invalid backend configuration: %s", err)
Expand All @@ -191,6 +199,28 @@ func readTfplan(r io.Reader) (*plans.Plan, error) {
Config: config,
Workspace: rawBackend.Workspace,
}
case rawPlan.StateStore != nil:
rawStateStore := rawPlan.StateStore
config, err := valueFromTfplan(rawStateStore.Config)
if err != nil {
return nil, fmt.Errorf("plan file has invalid state_store configuration: %s", err)
}
provider := &plans.Provider{}
err = provider.SetSource(rawStateStore.Provider.Source)
if err != nil {
return nil, fmt.Errorf("plan file has invalid state_store provider source: %s", err)
}
err = provider.SetVersion(rawStateStore.Provider.Version)
if err != nil {
return nil, fmt.Errorf("plan file has invalid state_store provider version: %s", err)
}

plan.StateStore = plans.StateStore{
Type: rawStateStore.Type,
Provider: provider,
Config: config,
Workspace: rawStateStore.Workspace,
}
}

if plan.Timestamp, err = time.Parse(time.RFC3339, rawPlan.Timestamp); err != nil {
Expand Down Expand Up @@ -636,17 +666,35 @@ func writeTfplan(plan *plans.Plan, w io.Writer) error {
)
}

if plan.Backend.Type == "" || plan.Backend.Config == nil {
// Store details about accessing state
backendInUse := plan.Backend.Type != "" && plan.Backend.Config != nil
stateStoreInUse := plan.StateStore.Type != "" && plan.StateStore.Config != nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the variable extraction here along with great names.

switch {
case !backendInUse && !stateStoreInUse:
// This suggests a bug in the code that created the plan, since it
// ought to always have a backend populated, even if it's the default
// ought to always have either a backend or state_store populated, even if it's the default
// "local" backend with a local state file.
return fmt.Errorf("plan does not have a backend configuration")
}

rawPlan.Backend = &planproto.Backend{
Type: plan.Backend.Type,
Config: valueToTfplan(plan.Backend.Config),
Workspace: plan.Backend.Workspace,
return fmt.Errorf("plan does not have a backend or state_store configuration")
case backendInUse && stateStoreInUse:
// This suggests a bug in the code that created the plan, since it
// should never have both a backend and state_store populated.
return fmt.Errorf("plan contains both backend and state_store configurations, only one is expected")
case backendInUse:
rawPlan.Backend = &planproto.Backend{
Type: plan.Backend.Type,
Config: valueToTfplan(plan.Backend.Config),
Workspace: plan.Backend.Workspace,
}
case stateStoreInUse:
rawPlan.StateStore = &planproto.StateStore{
Type: plan.StateStore.Type,
Provider: &planproto.Provider{
Version: plan.StateStore.Provider.Version.String(),
Source: plan.StateStore.Provider.Source.String(),
},
Config: valueToTfplan(plan.StateStore.Config),
Workspace: plan.StateStore.Workspace,
}
}

rawPlan.Timestamp = plan.Timestamp.Format(time.RFC3339)
Expand Down
Loading