Skip to content
Open
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
14 changes: 10 additions & 4 deletions internal/backend/backendrun/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,20 @@ type Operation struct {

// PlanId is an opaque value that backends can use to execute a specific
// plan for an apply operation.
//
PlanId string
PlanRefresh bool // PlanRefresh will do a refresh before a plan
PlanOutPath string // PlanOutPath is the path to save the plan

// 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
// Only one of PlanOutBackend or PlanOutStateStore may be set.
PlanOutBackend *plans.Backend

// PlanOutStateStore is the state_store to store with the plan. This is the
// state store that will be used when applying the plan.
// Only one of PlanOutBackend or PlanOutStateStore may be set
PlanOutStateStore *plans.StateStore

// ConfigDir is the path to the directory containing the configuration's
// root module.
ConfigDir string
Expand Down
14 changes: 10 additions & 4 deletions internal/backend/local/backend_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,22 @@ func (b *Local) opPlan(

// Save the plan to disk
if path := op.PlanOutPath; path != "" {
if op.PlanOutBackend == nil {
switch {
case op.PlanOutStateStore != nil:
plan.StateStore = op.PlanOutStateStore
case op.PlanOutBackend != nil:
plan.Backend = op.PlanOutBackend
default:
// This is always a bug in the operation caller; it's not valid
// to set PlanOutPath without also setting PlanOutBackend.
// to set PlanOutPath without also setting PlanOutStateStore or PlanOutBackend.
// Even when there is no state_store or backend block in the configuration, there should be a PlanOutBackend
// describing the implied local backend.
diags = diags.Append(fmt.Errorf(
"PlanOutPath set without also setting PlanOutBackend (this is a bug in Terraform)"),
"PlanOutPath set without also setting PlanOutStateStore or PlanOutBackend (this is a bug in Terraform)"),
)
op.ReportResult(runningOp, diags)
return
}
plan.Backend = op.PlanOutBackend

// 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
153 changes: 153 additions & 0 deletions internal/backend/local/backend_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

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

"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend/backendrun"
"github.com/hashicorp/terraform/internal/command/arguments"
Expand Down Expand Up @@ -913,3 +914,155 @@ func TestLocal_invalidOptions(t *testing.T) {
t.Fatal("expected error output")
}
}

// Checks if the state store info set on an Operation makes it into the resulting Plan
func TestLocal_plan_withStateStore(t *testing.T) {
b := TestLocal(t)

// Note: the mock provider doesn't include an implementation of
// pluggable state storage, but that's not needed for this test.
TestLocalProvider(t, b, "test", planFixtureSchema())
mockAddr := addrs.NewDefaultProvider("test")
providerVersion := version.Must(version.NewSemver("0.0.1"))
storeType := "test_foobar"
defaultWorkspace := "default"

testStateFile(t, b.StatePath, testPlanState_withDataSource())

outDir := t.TempDir()
planPath := filepath.Join(outDir, "plan.tfplan")

// Note: the config doesn't include a state_store block. Instead,
// that data is provided below when assigning a value to op.PlanOutStateStore.
// Usually that data is set as a result of parsing configuration.
op, configCleanup, _ := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
op.PlanMode = plans.NormalMode
op.PlanRefresh = true
op.PlanOutPath = planPath
storeCfg := cty.ObjectVal(map[string]cty.Value{
"path": cty.StringVal(b.StatePath),
})
storeCfgRaw, err := plans.NewDynamicValue(storeCfg, storeCfg.Type())
if err != nil {
t.Fatal(err)
}
providerCfg := cty.ObjectVal(map[string]cty.Value{}) // Empty as the mock provider has no schema for the provider
providerCfgRaw, err := plans.NewDynamicValue(providerCfg, providerCfg.Type())
if err != nil {
t.Fatal(err)
}
op.PlanOutStateStore = &plans.StateStore{
Type: storeType,
Config: storeCfgRaw,
Provider: &plans.Provider{
Source: &mockAddr,
Version: providerVersion,
Config: providerCfgRaw,
},
Workspace: defaultWorkspace,
}

run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result != backendrun.OperationSuccess {
t.Fatalf("plan operation failed")
}

if run.PlanEmpty {
t.Fatal("plan should not be empty")
}

plan := testReadPlan(t, planPath)

// The plan should contain details about the state store
if plan.StateStore == nil {
t.Fatalf("Expected plan to describe a state store, but data was missing")
}
// The plan should NOT contain details about a backend
if plan.Backend != nil {
t.Errorf("Expected plan to not describe a backend because a state store is in use, but data was present:\n plan.Backend = %v", plan.Backend)
}

if plan.StateStore.Type != storeType {
t.Errorf("Expected plan to describe a state store with type %s, but got %s", storeType, plan.StateStore.Type)
}
if plan.StateStore.Workspace != defaultWorkspace {
t.Errorf("Expected plan to describe a state store with workspace %s, but got %s", defaultWorkspace, plan.StateStore.Workspace)
}
if !plan.StateStore.Provider.Source.Equals(mockAddr) {
t.Errorf("Expected plan to describe a state store with provider address %s, but got %s", mockAddr, plan.StateStore.Provider.Source)
}
if !plan.StateStore.Provider.Version.Equal(providerVersion) {
t.Errorf("Expected plan to describe a state store with provider version %s, but got %s", providerVersion, plan.StateStore.Provider.Version)
}
}

// Checks if the backend info set on an Operation makes it into the resulting Plan
func TestLocal_plan_withBackend(t *testing.T) {
b := TestLocal(t)

TestLocalProvider(t, b, "test", planFixtureSchema())

testStateFile(t, b.StatePath, testPlanState_withDataSource())

outDir := t.TempDir()
planPath := filepath.Join(outDir, "plan.tfplan")

// Note: the config doesn't include a backend block. Instead,
// that data is provided below when assigning a value to op.PlanOutBackend.
// Usually that data is set as a result of parsing configuration.
op, configCleanup, _ := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
op.PlanMode = plans.NormalMode
op.PlanRefresh = true
op.PlanOutPath = planPath
cfg := cty.ObjectVal(map[string]cty.Value{
"path": cty.StringVal(b.StatePath),
})
cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
if err != nil {
t.Fatal(err)
}
backendType := "foobar"
defaultWorkspace := "default"
op.PlanOutBackend = &plans.Backend{
Type: backendType,
Config: cfgRaw,
Workspace: defaultWorkspace,
}

run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Result != backendrun.OperationSuccess {
t.Fatalf("plan operation failed")
}

if run.PlanEmpty {
t.Fatal("plan should not be empty")
}

plan := testReadPlan(t, planPath)

// The plan should contain details about the backend
if plan.Backend == nil {
t.Fatalf("Expected plan to describe a backend, but data was missing")
}
// The plan should NOT contain details about a state store
if plan.StateStore != nil {
t.Errorf("Expected plan to not describe a state store because a backend is in use, but data was present:\n plan.StateStore = %v", plan.StateStore)
}

if plan.Backend.Type != backendType {
t.Errorf("Expected plan to describe a backend with type %s, but got %s", backendType, plan.Backend.Type)
}
if plan.Backend.Workspace != defaultWorkspace {
t.Errorf("Expected plan to describe a backend with workspace %s, but got %s", defaultWorkspace, plan.Backend.Workspace)
}
}
8 changes: 6 additions & 2 deletions internal/command/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,12 @@ 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.
// This is used when creating plan files.
backendConfigState *workdir.BackendConfigState
// stateStoreConfigState is the currently active state_store state.
// This is used when creating plan files.
stateStoreConfigState *workdir.StateStoreConfigState

// Variables for the context (private)
variableArgs arguments.FlagNameValueSlice
Expand Down
80 changes: 66 additions & 14 deletions internal/command/meta_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/backend/backendrun"
backendInit "github.com/hashicorp/terraform/internal/backend/init"
"github.com/hashicorp/terraform/internal/backend/local"
backendLocal "github.com/hashicorp/terraform/internal/backend/local"
backendPluggable "github.com/hashicorp/terraform/internal/backend/pluggable"
"github.com/hashicorp/terraform/internal/cloud"
Expand All @@ -37,6 +38,7 @@ import (
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/command/workdir"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/didyoumean"
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
Expand Down Expand Up @@ -221,13 +223,14 @@ 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 {
backendInUse := opts.StateStoreConfig == nil
if backendInUse && 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 @@ -440,13 +443,42 @@ 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.PlanData(schema, nil, 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:
// Both set
panic("failed to encode backend configuration for plan: both backend and state_store data present but they are mutually exclusive")
case m.stateStoreConfigState != nil:
// To access the provider schema, we need to access the underlying backends
var providerSchema *configschema.Block
if lb, ok := b.(*local.Local); ok {
if p, ok := lb.Backend.(*backendPluggable.Pluggable); ok {
providerSchema = p.ProviderSchema()
}
}

// TODO: do we need to protect against a nil provider schema? When a provider has an empty schema does that present as nil?

planOutStateStore, err = m.stateStoreConfigState.PlanData(schema, providerSchema, 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))
}
default:
// Either backendConfigState is set, or it's nil; PlanData method can handle either.
planOutBackend, err = m.backendConfigState.PlanData(schema, nil, 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))
}
}

stateLocker := clistate.NewNoopLocker()
Expand All @@ -465,15 +497,24 @@ func (m *Meta) Operation(b backend.Backend, vt arguments.ViewType) *backendrun.O
log.Printf("[WARN] Failed to load dependency locks while preparing backend operation (ignored): %s", diags.Err().Error())
}

return &backendrun.Operation{
PlanOutBackend: planOutBackend,
op := &backendrun.Operation{
// These two fields are mutually exclusive; one is being assigned a nil value below.
PlanOutBackend: planOutBackend,
PlanOutStateStore: planOutStateStore,

Targets: m.targets,
UIIn: m.UIInput(),
UIOut: m.Ui,
Workspace: workspace,
StateLocker: stateLocker,
DependencyLocks: depLocks,
}

if op.PlanOutBackend != nil && op.PlanOutStateStore != nil {
panic("failed to prepare operation: both backend and state_store configurations are present")
}

return op
}

// backendConfig returns the local configuration for the backend
Expand Down Expand Up @@ -727,10 +768,21 @@ 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
m.stateStoreConfigState = nil
defer func() {
if s := sMgr.State(); s != nil && !s.Backend.Empty() {
m.backendState = s.Backend
s := sMgr.State()
switch {
case s == nil:
// Do nothing

// TODO: Should we add a synthetic object here,
// as part of addressing actions described in this FIXME?
// https://github.com/hashicorp/terraform/blob/053738fbf08d50261eccb463580525b88f461d8e/internal/command/meta_backend.go#L222-L243
case !s.Backend.Empty():
m.backendConfigState = s.Backend
case !s.StateStore.Empty():
m.stateStoreConfigState = s.StateStore
}
}()

Expand Down
Loading
Loading