diff --git a/backend/local/backend_plan.go b/backend/local/backend_plan.go index 815ad7389..3598cceca 100644 --- a/backend/local/backend_plan.go +++ b/backend/local/backend_plan.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/command/format" + "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/terraform" ) @@ -28,6 +29,18 @@ func (b *Local) opPlan( "directory as an argument.\n\n")) } + // A local plan requires either a plan or a module + if op.Plan == nil && op.Module == nil && !op.Destroy { + runningOp.Err = fmt.Errorf(strings.TrimSpace(planErrNoConfig)) + return + } + + // If we have a nil module at this point, then set it to an empty tree + // to avoid any potential crashes. + if op.Module == nil { + op.Module = module.NewEmptyTree() + } + // Setup our count hook that keeps track of resource changes countHook := new(CountHook) if b.ContextOpts == nil { @@ -120,6 +133,16 @@ func (b *Local) opPlan( } } +const planErrNoConfig = ` +No configuration files found! + +Plan requires configuration to be present. Planning without a configuration +would mark everything for destruction, which is normally not what is desired. +If you would like to destroy everything, please run plan with the "-destroy" +flag or create a single empty configuration file. Otherwise, please create +a Terraform configuration file in the path being executed and try again. +` + const planHeaderNoOutput = ` The Terraform execution plan has been generated and is shown below. Resources are shown in alphabetical order for quick scanning. Green resources diff --git a/backend/local/backend_plan_test.go b/backend/local/backend_plan_test.go index 20e0420a2..144260308 100644 --- a/backend/local/backend_plan_test.go +++ b/backend/local/backend_plan_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "path/filepath" + "strings" "testing" "github.com/hashicorp/terraform/backend" @@ -36,6 +37,29 @@ func TestLocal_planBasic(t *testing.T) { } } +func TestLocal_planNoConfig(t *testing.T) { + b := TestLocal(t) + TestLocalProvider(t, b, "test") + + op := testOperationPlan() + op.Module = nil + op.PlanRefresh = true + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("bad: %s", err) + } + <-run.Done() + + err = run.Err + if err == nil { + t.Fatal("should error") + } + if !strings.Contains(err.Error(), "configuration") { + t.Fatalf("bad: %s", err) + } +} + func TestLocal_planRefreshFalse(t *testing.T) { b := TestLocal(t) p := TestLocalProvider(t, b, "test") @@ -110,6 +134,48 @@ func TestLocal_planDestroy(t *testing.T) { } } +func TestLocal_planDestroyNoConfig(t *testing.T) { + b := TestLocal(t) + p := TestLocalProvider(t, b, "test") + terraform.TestStateFile(t, b.StatePath, testPlanState()) + + outDir := testTempDir(t) + defer os.RemoveAll(outDir) + planPath := filepath.Join(outDir, "plan.tfplan") + + op := testOperationPlan() + op.Destroy = true + op.PlanRefresh = true + op.Module = nil + op.PlanOutPath = planPath + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("bad: %s", err) + } + <-run.Done() + if run.Err != nil { + t.Fatalf("err: %s", err) + } + + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } + + if run.PlanEmpty { + t.Fatal("plan should not be empty") + } + + plan := testReadPlan(t, planPath) + for _, m := range plan.Diff.Modules { + for _, r := range m.Resources { + if !r.Destroy { + t.Fatalf("bad: %#v", r) + } + } + } +} + func TestLocal_planOutPathNoChange(t *testing.T) { b := TestLocal(t) TestLocalProvider(t, b, "test")