Skip to content
Draft
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
9 changes: 5 additions & 4 deletions internal/backend/backendrun/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,11 @@ type Operation struct {
//
// PlanOutBackend is the backend to store with the plan. This is the
// backend that will be used when applying the plan.
PlanId string
PlanRefresh bool // PlanRefresh will do a refresh before a plan
PlanOutPath string // PlanOutPath is the path to save the plan
PlanOutBackend *plans.Backend
PlanId string
PlanRefresh bool // PlanRefresh will do a refresh before a plan
PlanOutPath string // PlanOutPath is the path to save the plan
PlanOutBackend *plans.Backend
PlanOutStateStore *plans.StateStore

// ConfigDir is the path to the directory containing the configuration's
// root module.
Expand Down
9 changes: 8 additions & 1 deletion internal/backend/local/backend_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,14 @@ func (b *Local) opPlan(
op.ReportResult(runningOp, diags)
return
}
plan.Backend = *op.PlanOutBackend
switch {
case op.PlanOutBackend != nil:
plan.Backend = *op.PlanOutBackend
case op.PlanOutStateStore != nil:
plan.StateStore = *op.PlanOutStateStore
default:
panic("backend and state_store configuration data missing from Operation")
}

// We may have updated the state in the refresh step above, but we
// will freeze that updated state in the plan file for now and
Expand Down
6 changes: 3 additions & 3 deletions internal/command/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,16 +210,16 @@ func (c *ApplyCommand) PrepareBackend(planFile *planfile.WrappedPlanFile, args *
))
return nil, diags
}
if plan.Backend.Config == nil {
if plan.Backend.Config == nil && plan.StateStore.Config == nil {
// Should never happen; always indicates a bug in the creation of the plan file
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to read plan from plan file",
"The given plan file does not have a valid backend configuration. This is a bug in the Terraform command that generated this plan file.",
"The given plan file does not have either a valid backend or state_store configuration. This is a bug in the Terraform command that generated this plan file.",
))
return nil, diags
}
be, beDiags = c.BackendForLocalPlan(plan.Backend)
be, beDiags = c.BackendForLocalPlan(plan)
} else {
// Both new plans and saved cloud plans load their backend from config.
backendConfig, configDiags := c.loadBackendConfig(".")
Expand Down
5 changes: 3 additions & 2 deletions internal/command/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,9 @@ type Meta struct {
// It is initialized on first use.
configLoader *configload.Loader

// backendState is the currently active backend state
backendState *workdir.BackendConfigState
// backendConfigState is the currently active backend state
backendConfigState *workdir.BackendConfigState
stateStoreConfigState *workdir.StateStoreConfigState

// Variables for the context (private)
variableArgs arguments.FlagNameValueSlice
Expand Down
75 changes: 55 additions & 20 deletions internal/command/meta_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,13 @@ func (m *Meta) Backend(opts *BackendOpts) (backendrun.OperationsBackend, tfdiags
// the user, since the local backend should only be used when learning or
// in exceptional cases and so it's better to help the user learn that
// by introducing it as a concept.
if m.backendState == nil {
if m.backendConfigState == nil {
// NOTE: This synthetic object is intentionally _not_ retained in the
// on-disk record of the backend configuration, which was already dealt
// with inside backendFromConfig, because we still need that codepath
// to be able to recognize the lack of a config as distinct from
// explicitly setting local until we do some more refactoring here.
m.backendState = &workdir.BackendConfigState{
m.backendConfigState = &workdir.BackendConfigState{
Type: "local",
ConfigRaw: json.RawMessage("{}"),
}
Expand Down Expand Up @@ -302,19 +302,28 @@ func (m *Meta) selectWorkspace(b backend.Backend) error {
// The current workspace name is also stored as part of the plan, and so this
// method will check that it matches the currently-selected workspace name
// and produce error diagnostics if not.
func (m *Meta) BackendForLocalPlan(settings plans.Backend) (backendrun.OperationsBackend, tfdiags.Diagnostics) {
func (m *Meta) BackendForLocalPlan(plan *plans.Plan) (backendrun.OperationsBackend, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

f := backendInit.Backend(settings.Type)
if f == nil {
diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), settings.Type))
return nil, diags
var b backend.Backend
var config plans.DynamicValue
if plan.StateStore.Config != nil {
// TODO - code for state_store
} else if plan.Backend.Config != nil {
settings := plan.Backend
config = plan.Backend.Config

f := backendInit.Backend(settings.Type)
if f == nil {
diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), settings.Type))
return nil, diags
}
b = f()
log.Printf("[TRACE] Meta.BackendForLocalPlan: instantiated backend of type %T", b)
}
b := f()
log.Printf("[TRACE] Meta.BackendForLocalPlan: instantiated backend of type %T", b)

schema := b.ConfigSchema()
configVal, err := settings.Config.Decode(schema.ImpliedType())
configVal, err := config.Decode(schema.ImpliedType())
if err != nil {
diags = diags.Append(fmt.Errorf("saved backend configuration is invalid: %w", err))
return nil, diags
Expand Down Expand Up @@ -412,13 +421,34 @@ 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)
if err != nil {
// Always indicates an implementation error in practice, because
// errors here indicate invalid encoding of the backend configuration
// in memory, and we should always have validated that by the time
// we get here.
panic(fmt.Sprintf("failed to encode backend configuration for plan: %s", err))

var planOutBackend *plans.Backend
var planOutStateStore *plans.StateStore
switch {
case m.backendConfigState == nil && m.stateStoreConfigState == nil:
// It is valid for neither to be set.
// So we do nothing here.
case m.backendConfigState != nil && m.stateStoreConfigState != nil:
// Both set
panic("failed to encode backend configuration for plan: both backend and state_store data present but they are mutually exclusive")
case m.backendConfigState != nil:
planOutBackend, err = m.backendConfigState.Plan(schema, workspace)
if err != nil {
// Always indicates an implementation error in practice, because
// errors here indicate invalid encoding of the backend configuration
// in memory, and we should always have validated that by the time
// we get here.
panic(fmt.Sprintf("failed to encode backend configuration for plan: %s", err))
}
case m.stateStoreConfigState != nil:
planOutStateStore, err = m.stateStoreConfigState.Plan(schema, workspace)
if err != nil {
// Always indicates an implementation error in practice, because
// errors here indicate invalid encoding of the state_store configuration
// in memory, and we should always have validated that by the time
// we get here.
panic(fmt.Sprintf("failed to encode state_store configuration for plan: %s", err))
}
}

stateLocker := clistate.NewNoopLocker()
Expand All @@ -438,7 +468,12 @@ func (m *Meta) Operation(b backend.Backend, vt arguments.ViewType) *backendrun.O
}

return &backendrun.Operation{
PlanOutBackend: planOutBackend,

// These two fields are mutually exclusive,
// and one is being assigned a nil value below.
PlanOutBackend: planOutBackend,
PlanOutStateStore: planOutStateStore,

Targets: m.targets,
UIIn: m.UIInput(),
UIOut: m.Ui,
Expand Down Expand Up @@ -578,10 +613,10 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di

// Upon return, we want to set the state we're using in-memory so that
// we can access it for commands.
m.backendState = nil
m.backendConfigState = nil
defer func() {
if s := sMgr.State(); s != nil && !s.Backend.Empty() {
m.backendState = s.Backend
m.backendConfigState = s.Backend
}
}()

Expand Down
36 changes: 21 additions & 15 deletions internal/command/meta_backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1543,17 +1543,19 @@ func TestMetaBackend_planLocal(t *testing.T) {
if err != nil {
t.Fatal(err)
}
backendConfig := plans.Backend{
Type: "local",
Config: backendConfigRaw,
Workspace: "default",
plan := &plans.Plan{
Backend: plans.Backend{
Type: "local",
Config: backendConfigRaw,
Workspace: "default",
},
}

// Setup the meta
m := testMetaBackend(t, nil)

// Get the backend
b, diags := m.BackendForLocalPlan(backendConfig)
b, diags := m.BackendForLocalPlan(plan)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
Expand Down Expand Up @@ -1634,10 +1636,12 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) {
if err != nil {
t.Fatal(err)
}
plannedBackend := plans.Backend{
Type: "local",
Config: backendConfigRaw,
Workspace: "default",
plan := &plans.Plan{
Backend: plans.Backend{
Type: "local",
Config: backendConfigRaw,
Workspace: "default",
},
}

// Create an alternate output path
Expand All @@ -1654,7 +1658,7 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) {
m.stateOutPath = statePath

// Get the backend
b, diags := m.BackendForLocalPlan(plannedBackend)
b, diags := m.BackendForLocalPlan(plan)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
Expand Down Expand Up @@ -1733,17 +1737,19 @@ func TestMetaBackend_planLocalMatch(t *testing.T) {
if err != nil {
t.Fatal(err)
}
backendConfig := plans.Backend{
Type: "local",
Config: backendConfigRaw,
Workspace: "default",
plan := &plans.Plan{
Backend: plans.Backend{
Type: "local",
Config: backendConfigRaw,
Workspace: "default",
},
}

// Setup the meta
m := testMetaBackend(t, nil)

// Get the backend
b, diags := m.BackendForLocalPlan(backendConfig)
b, diags := m.BackendForLocalPlan(plan)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
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 _ Planner[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
// Plan 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) Plan(schema *configschema.Block, workspaceName string) (*plans.Backend, error) {
if s == nil {
return nil, nil
}
Expand Down
15 changes: 12 additions & 3 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)
}

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

// Planner implementations can return a representation of their data that's
// appropriate for storing in a plan file.
type Planner[T any] interface {
Plan(*configschema.Block, 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 _ Planner[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
// Plan 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) Plan(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
Loading