Skip to content
Closed
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)
}
}
4 changes: 2 additions & 2 deletions internal/command/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@ type Meta struct {
// It is initialized on first use.
configLoader *configload.Loader

// backendState is the currently active backend state
backendState *workdir.BackendConfigState
// The latest backend configuration state file contents
backendStateFile *workdir.BackendStateFile

// Variables for the context (private)
variableArgs arguments.FlagNameValueSlice
Expand Down
Loading