backend/local: Require caller to set PlanOutBackend with PlanOutPath

We can't generate a valid plan file without a backend configuration to
write into it, but it's the responsibility of the caller (the command
package) to manage the backend configuration mechanism, so we require it
to tell us what to write here.

This feels a little strange because the backend in principle knows its
own config, but in practice the backend only knows the _processed_ version
of the config, not the raw configuration value that was used to configure
it.
This commit is contained in:
Martin Atkins 2018-10-09 12:19:24 -07:00
parent 88984aaca0
commit 2b80df0163
4 changed files with 44 additions and 5 deletions

View File

@ -114,12 +114,14 @@ func (b *Local) opPlan(
// Save the plan to disk
if path := op.PlanOutPath; path != "" {
if op.PlanOutBackend != nil {
plan.Backend = *op.PlanOutBackend
} else {
op.PlanOutBackend = &plans.Backend{}
plan.Backend = *op.PlanOutBackend
if op.PlanOutBackend == nil {
// This is always a bug in the operation caller; it's not valid
// to set PlanOutPath without also setting PlanOutBackend.
diags = diags.Append(fmt.Errorf("PlanOutPath set without also setting PlanOutBackend (this is a bug in Terraform)"))
b.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

View File

@ -174,6 +174,18 @@ func TestLocal_planDestroy(t *testing.T) {
op.Destroy = true
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)
}
op.PlanOutBackend = &plans.Backend{
// Just a placeholder so that we can generate a valid plan file.
Type: "local",
Config: cfgRaw,
}
run, err := b.Operation(context.Background(), op)
if err != nil {
@ -213,6 +225,18 @@ func TestLocal_planOutPathNoChange(t *testing.T) {
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
defer configCleanup()
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)
}
op.PlanOutBackend = &plans.Backend{
// Just a placeholder so that we can generate a valid plan file.
Type: "local",
Config: cfgRaw,
}
run, err := b.Operation(context.Background(), op)
if err != nil {
@ -314,6 +338,8 @@ func testPlanState() *states.State {
}
func testReadPlan(t *testing.T, path string) *plans.Plan {
t.Helper()
p, err := planfile.Open(path)
if err != nil {
t.Fatalf("err: %s", err)

View File

@ -34,6 +34,10 @@ func TestLocal(t *testing.T) (*Local, func()) {
var diags tfdiags.Diagnostics
diags = diags.Append(vals...)
for _, diag := range diags {
// NOTE: Since the caller here is not directly the TestLocal
// function, t.Helper doesn't apply and so the log source
// isn't correctly shown in the test log output. This seems
// unavoidable as long as this is happening so indirectly.
t.Log(diag.Description().Summary)
if local.CLI != nil {
local.CLI.Error(diag.Description().Summary)

View File

@ -351,6 +351,13 @@ func writeTfplan(plan *plans.Plan, w io.Writer) error {
rawPlan.Variables[name] = valueToTfplan(val)
}
if plan.Backend.Type == "" || plan.Backend.Config == nil {
// 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
// "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),