return early from opPlan when the plan is nil

While the returned plan is checked for nil in most cases, there was
a single point where the plan was dereferenced which could panic. Rather
than always guarding the dereferences, return early when the plan is
nil.
This commit is contained in:
James Bardin 2023-03-10 09:34:47 -05:00
parent 189820b6ea
commit c02e7e8754
3 changed files with 42 additions and 11 deletions

View File

@ -366,8 +366,9 @@ type RunningOperation struct {
// operation has completed.
Result OperationResult
// PlanEmpty is populated after a Plan operation completes without error
// to note whether a plan is empty or has changes.
// PlanEmpty is populated after a Plan operation completes to note whether
// a plan is empty or has changes. This is only used in the CLI to determine
// the exit status because the plan value is not available at that point.
PlanEmpty bool
// State is the final state after the operation completed. Persisting

View File

@ -100,11 +100,19 @@ func (b *Local) opPlan(
// generate a partial saved plan file for external analysis.
diags = diags.Append(planDiags)
// Even if there are errors we need to handle anything that may be
// contained within the plan, so only exit if there is no data at all.
if plan == nil {
runningOp.PlanEmpty = true
op.ReportResult(runningOp, diags)
return
}
// Record whether this plan includes any side-effects that could be applied.
runningOp.PlanEmpty = !plan.CanApply()
// Save the plan to disk
if path := op.PlanOutPath; path != "" && plan != nil {
if path := op.PlanOutPath; path != "" {
if op.PlanOutBackend == nil {
// This is always a bug in the operation caller; it's not valid
// to set PlanOutPath without also setting PlanOutBackend.
@ -154,15 +162,13 @@ func (b *Local) opPlan(
// Render the plan, if we produced one.
// (This might potentially be a partial plan with Errored set to true)
if plan != nil {
schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
op.ReportResult(runningOp, diags)
return
}
op.View.Plan(plan, schemas)
schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
op.ReportResult(runningOp, diags)
return
}
op.View.Plan(plan, schemas)
// If we've accumulated any diagnostics along the way then we'll show them
// here just before we show the summary and next steps. This can potentially

View File

@ -880,3 +880,27 @@ func planFixtureSchema() *terraform.ProviderSchema {
},
}
}
func TestLocal_invalidOptions(t *testing.T) {
b := TestLocal(t)
TestLocalProvider(t, b, "test", planFixtureSchema())
op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
op.PlanRefresh = true
op.PlanMode = plans.RefreshOnlyMode
op.ForceReplace = []addrs.AbsResourceInstance{mustResourceInstanceAddr("test_instance.foo")}
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
<-run.Done()
if run.Result == backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if errOutput := done(t).Stderr(); errOutput == "" {
t.Fatal("expected error output")
}
}