command+backend: generalized "plan mode"

So far we've only had "normal mode" and "destroy mode", where the latter
is activated either by "terraform plan -destroy" or "terraform destroy".

In preparation for introducing a third mode "refresh only" this
generalizes how we handle modes so we can potentially deal with an
arbitrary number of modes, although for now we only intend to have three.

Mostly this is just a different implementation of the same old behavior,
but there is one small user-visible difference here: the "terraform apply"
command now accepts a -destroy option, mirroring the option of the same
name on "terraform plan", which in turn makes "terraform destroy"
effectively a shorthand for "terraform apply -destroy".

This is intended to make us consistent that "terraform apply" without a
plan file argument accepts all of the same plan-customization options that
"terraform plan" does, which will in turn avoid us having to add a new
alias of "terraform plan" for each new plan mode we might add. The -help
output is changed in that vein here, although we'll wait for subsequent
commit to make a similar change to the website documentation just so we
can deal with the "refresh only mode" docs at the same time.
This commit is contained in:
Martin Atkins 2021-04-05 16:28:59 -07:00
parent c6a7d080d9
commit 89f986ded6
21 changed files with 299 additions and 129 deletions

View File

@ -193,8 +193,8 @@ type Operation struct {
// The options below are more self-explanatory and affect the runtime // The options below are more self-explanatory and affect the runtime
// behavior of the operation. // behavior of the operation.
PlanMode plans.Mode
AutoApprove bool AutoApprove bool
Destroy bool
Parallelism int Parallelism int
Targets []addrs.Targetable Targets []addrs.Targetable
Variables map[string]UnparsedVariableValue Variables map[string]UnparsedVariableValue

View File

@ -8,6 +8,7 @@ import (
"github.com/hashicorp/errwrap" "github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/command/views"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/states/statefile" "github.com/hashicorp/terraform/states/statefile"
"github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/states/statemgr"
@ -26,7 +27,7 @@ func (b *Local) opApply(
// If we have a nil module at this point, then set it to an empty tree // If we have a nil module at this point, then set it to an empty tree
// to avoid any potential crashes. // to avoid any potential crashes.
if op.PlanFile == nil && !op.Destroy && !op.HasConfig() { if op.PlanFile == nil && op.PlanMode != plans.DestroyMode && !op.HasConfig() {
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, tfdiags.Error,
"No configuration files", "No configuration files",
@ -76,7 +77,7 @@ func (b *Local) opApply(
mustConfirm := hasUI && !op.AutoApprove && !trivialPlan mustConfirm := hasUI && !op.AutoApprove && !trivialPlan
if mustConfirm { if mustConfirm {
var desc, query string var desc, query string
if op.Destroy { if op.PlanMode == plans.DestroyMode {
if op.Workspace != "default" { if op.Workspace != "default" {
query = "Do you really want to destroy all resources in workspace \"" + op.Workspace + "\"?" query = "Do you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
} else { } else {
@ -116,7 +117,7 @@ func (b *Local) opApply(
return return
} }
if v != "yes" { if v != "yes" {
op.View.Cancelled(op.Destroy) op.View.Cancelled(op.PlanMode)
runningOp.Result = backend.OperationFailure runningOp.Result = backend.OperationFailure
return return
} }

View File

@ -18,6 +18,7 @@ import (
"github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/states/statemgr"
@ -115,7 +116,7 @@ func TestLocal_applyEmptyDirDestroy(t *testing.T) {
op, configCleanup, done := testOperationApply(t, "./testdata/empty") op, configCleanup, done := testOperationApply(t, "./testdata/empty")
defer configCleanup() defer configCleanup()
op.Destroy = true op.PlanMode = plans.DestroyMode
run, err := b.Operation(context.Background(), op) run, err := b.Operation(context.Background(), op)
if err != nil { if err != nil {

View File

@ -10,7 +10,6 @@ import (
"github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/plans/planfile"
"github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
@ -66,12 +65,7 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload.
} }
// Copy set options from the operation // Copy set options from the operation
switch { opts.PlanMode = op.PlanMode
case op.Destroy:
opts.PlanMode = plans.DestroyMode
default:
opts.PlanMode = plans.NormalMode
}
opts.Targets = op.Targets opts.Targets = op.Targets
opts.UIInput = op.UIIn opts.UIInput = op.UIIn
opts.Hooks = op.Hooks opts.Hooks = op.Hooks

View File

@ -35,7 +35,7 @@ func (b *Local) opPlan(
} }
// Local planning requires a config, unless we're planning to destroy. // Local planning requires a config, unless we're planning to destroy.
if !op.Destroy && !op.HasConfig() { if op.PlanMode != plans.DestroyMode && !op.HasConfig() {
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, tfdiags.Error,
"No configuration files", "No configuration files",

View File

@ -544,7 +544,7 @@ func TestLocal_planDestroy(t *testing.T) {
op, configCleanup, done := testOperationPlan(t, "./testdata/plan") op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup() defer configCleanup()
op.Destroy = true op.PlanMode = plans.DestroyMode
op.PlanRefresh = true op.PlanRefresh = true
op.PlanOutPath = planPath op.PlanOutPath = planPath
cfg := cty.ObjectVal(map[string]cty.Value{ cfg := cty.ObjectVal(map[string]cty.Value{
@ -598,7 +598,7 @@ func TestLocal_planDestroy_withDataSources(t *testing.T) {
op, configCleanup, done := testOperationPlan(t, "./testdata/destroy-with-ds") op, configCleanup, done := testOperationPlan(t, "./testdata/destroy-with-ds")
defer configCleanup() defer configCleanup()
op.Destroy = true op.PlanMode = plans.DestroyMode
op.PlanRefresh = true op.PlanRefresh = true
op.PlanOutPath = planPath op.PlanOutPath = planPath
cfg := cty.ObjectVal(map[string]cty.Value{ cfg := cty.ObjectVal(map[string]cty.Value{

View File

@ -10,6 +10,7 @@ import (
tfe "github.com/hashicorp/go-tfe" tfe "github.com/hashicorp/go-tfe"
version "github.com/hashicorp/go-version" version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
) )
@ -84,7 +85,7 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
)) ))
} }
if !op.HasConfig() && !op.Destroy { if !op.HasConfig() && op.PlanMode != plans.DestroyMode {
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, tfdiags.Error,
"No configuration files found", "No configuration files found",
@ -152,10 +153,12 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
if r.Actions.IsDiscardable { if r.Actions.IsDiscardable {
err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{}) err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{})
if err != nil { if err != nil {
if op.Destroy { switch op.PlanMode {
case plans.DestroyMode:
return r, generalError("Failed to discard destroy", err) return r, generalError("Failed to discard destroy", err)
default:
return r, generalError("Failed to discard apply", err)
} }
return r, generalError("Failed to discard apply", err)
} }
} }
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
@ -176,7 +179,7 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
if mustConfirm { if mustConfirm {
opts := &terraform.InputOpts{Id: "approve"} opts := &terraform.InputOpts{Id: "approve"}
if op.Destroy { if op.PlanMode == plans.DestroyMode {
opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?" opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" + opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" +
"There is no undo. Only 'yes' will be accepted to confirm." "There is no undo. Only 'yes' will be accepted to confirm."

View File

@ -19,6 +19,7 @@ import (
"github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/command/views"
"github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/plans/planfile"
"github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
@ -968,7 +969,7 @@ func TestRemote_applyDestroy(t *testing.T) {
"approve": "yes", "approve": "yes",
}) })
op.Destroy = true op.PlanMode = plans.DestroyMode
op.UIIn = input op.UIIn = input
op.UIOut = b.CLI op.UIOut = b.CLI
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
@ -1014,7 +1015,7 @@ func TestRemote_applyDestroyNoConfig(t *testing.T) {
defer configCleanup() defer configCleanup()
defer done(t) defer done(t)
op.Destroy = true op.PlanMode = plans.DestroyMode
op.UIIn = input op.UIIn = input
op.UIOut = b.CLI op.UIOut = b.CLI
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName

View File

@ -13,6 +13,7 @@ import (
tfe "github.com/hashicorp/go-tfe" tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -508,7 +509,7 @@ func (b *Remote) confirm(stopCtx context.Context, op *backend.Operation, opts *t
if err == errRunDiscarded { if err == errRunDiscarded {
err = errApplyDiscarded err = errApplyDiscarded
if op.Destroy { if op.PlanMode == plans.DestroyMode {
err = errDestroyDiscarded err = errDestroyDiscarded
} }
} }
@ -551,7 +552,7 @@ func (b *Remote) confirm(stopCtx context.Context, op *backend.Operation, opts *t
if r.Actions.IsDiscardable { if r.Actions.IsDiscardable {
err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{}) err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{})
if err != nil { if err != nil {
if op.Destroy { if op.PlanMode == plans.DestroyMode {
return generalError("Failed to discard destroy", err) return generalError("Failed to discard destroy", err)
} }
return generalError("Failed to discard apply", err) return generalError("Failed to discard apply", err)
@ -560,7 +561,7 @@ func (b *Remote) confirm(stopCtx context.Context, op *backend.Operation, opts *t
// Even if the run was discarded successfully, we still // Even if the run was discarded successfully, we still
// return an error as the apply command was canceled. // return an error as the apply command was canceled.
if op.Destroy { if op.PlanMode == plans.DestroyMode {
return errDestroyDiscarded return errDestroyDiscarded
} }
return errApplyDiscarded return errApplyDiscarded

View File

@ -12,7 +12,6 @@ import (
"github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
@ -62,12 +61,7 @@ func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Fu
} }
// Copy set options from the operation // Copy set options from the operation
switch { opts.PlanMode = op.PlanMode
case op.Destroy:
opts.PlanMode = plans.DestroyMode
default:
opts.PlanMode = plans.NormalMode
}
opts.Targets = op.Targets opts.Targets = op.Targets
opts.UIInput = op.UIIn opts.UIInput = op.UIIn

View File

@ -17,6 +17,7 @@ import (
tfe "github.com/hashicorp/go-tfe" tfe "github.com/hashicorp/go-tfe"
version "github.com/hashicorp/go-version" version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
) )
@ -89,7 +90,7 @@ func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operatio
)) ))
} }
if !op.HasConfig() && !op.Destroy { if !op.HasConfig() && op.PlanMode != plans.DestroyMode {
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, tfdiags.Error,
"No configuration files found", "No configuration files found",
@ -238,12 +239,26 @@ in order to capture the filesystem context the remote workspace expects:
} }
runOptions := tfe.RunCreateOptions{ runOptions := tfe.RunCreateOptions{
IsDestroy: tfe.Bool(op.Destroy),
Message: tfe.String(queueMessage), Message: tfe.String(queueMessage),
ConfigurationVersion: cv, ConfigurationVersion: cv,
Workspace: w, Workspace: w,
} }
switch op.PlanMode {
case plans.NormalMode:
// okay, but we don't need to do anything special for this
case plans.DestroyMode:
runOptions.IsDestroy = tfe.Bool(true)
default:
// Shouldn't get here because we should update this for each new
// plan mode we add, mapping it to the corresponding RunCreateOptions
// field.
return nil, generalError(
"Invalid plan mode",
fmt.Errorf("remote backend doesn't support %s", op.PlanMode),
)
}
if len(op.Targets) != 0 { if len(op.Targets) != 0 {
runOptions.TargetAddrs = make([]string, 0, len(op.Targets)) runOptions.TargetAddrs = make([]string, 0, len(op.Targets))
for _, addr := range op.Targets { for _, addr := range op.Targets {

View File

@ -18,6 +18,7 @@ import (
"github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/command/views"
"github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/plans/planfile"
"github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
@ -709,7 +710,7 @@ func TestRemote_planDestroy(t *testing.T) {
defer configCleanup() defer configCleanup()
defer done(t) defer done(t)
op.Destroy = true op.PlanMode = plans.DestroyMode
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
run, err := b.Operation(context.Background(), op) run, err := b.Operation(context.Background(), op)
@ -734,7 +735,7 @@ func TestRemote_planDestroyNoConfig(t *testing.T) {
defer configCleanup() defer configCleanup()
defer done(t) defer done(t)
op.Destroy = true op.PlanMode = plans.DestroyMode
op.Workspace = backend.DefaultStateName op.Workspace = backend.DefaultStateName
run, err := b.Operation(context.Background(), op) run, err := b.Operation(context.Background(), op)

View File

@ -23,6 +23,8 @@ type ApplyCommand struct {
} }
func (c *ApplyCommand) Run(rawArgs []string) int { func (c *ApplyCommand) Run(rawArgs []string) int {
var diags tfdiags.Diagnostics
// Parse and apply global view arguments // Parse and apply global view arguments
common, rawArgs := arguments.ParseView(rawArgs) common, rawArgs := arguments.ParseView(rawArgs)
c.View.Configure(common) c.View.Configure(common)
@ -33,7 +35,13 @@ func (c *ApplyCommand) Run(rawArgs []string) int {
c.Meta.Color = c.Meta.color c.Meta.Color = c.Meta.color
// Parse and validate flags // Parse and validate flags
args, diags := arguments.ParseApply(rawArgs) var args *arguments.Apply
switch {
case c.Destroy:
args, diags = arguments.ParseApplyDestroy(rawArgs)
default:
args, diags = arguments.ParseApply(rawArgs)
}
// Instantiate the view, even if there are flag errors, so that we render // Instantiate the view, even if there are flag errors, so that we render
// diagnostics according to the desired view // diagnostics according to the desired view
@ -253,7 +261,7 @@ func (c *ApplyCommand) OperationRequest(
opReq := c.Operation(be) opReq := c.Operation(be)
opReq.AutoApprove = autoApprove opReq.AutoApprove = autoApprove
opReq.ConfigDir = "." opReq.ConfigDir = "."
opReq.Destroy = c.Destroy opReq.PlanMode = args.PlanMode
opReq.Hooks = view.Hooks() opReq.Hooks = view.Hooks()
opReq.PlanFile = planFile opReq.PlanFile = planFile
opReq.PlanRefresh = args.Refresh opReq.PlanRefresh = args.Refresh
@ -345,9 +353,6 @@ Options:
-parallelism=n Limit the number of parallel resource operations. -parallelism=n Limit the number of parallel resource operations.
Defaults to 10. Defaults to 10.
-refresh=true Update state prior to checking for differences. This
has no effect if a plan file is given to apply.
-state=path Path to read and save state (unless state-out -state=path Path to read and save state (unless state-out
is specified). Defaults to "terraform.tfstate". is specified). Defaults to "terraform.tfstate".
@ -355,18 +360,10 @@ Options:
"-state". This can be used to preserve the old "-state". This can be used to preserve the old
state. state.
-target=resource Resource to target. Operation will be limited to this If you don't provide a saved plan file then this command will also accept
resource and its dependencies. This flag can be used all of the plan-customization options accepted by the terraform plan command.
multiple times. For more information on those options, run:
terraform plan -help
-var 'foo=bar' Set a variable in the Terraform configuration. This
flag can be set multiple times.
-var-file=foo Set variables in the Terraform configuration from
a file. If "terraform.tfvars" or any ".auto.tfvars"
files are present, they will be automatically loaded.
` `
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)
} }
@ -377,35 +374,12 @@ Usage: terraform [global options] destroy [options]
Destroy Terraform-managed infrastructure. Destroy Terraform-managed infrastructure.
Options: This command is a convenience alias for:
terraform apply -destroy
-auto-approve Skip interactive approval before destroying. This command also accepts many of the plan-customization options accepted by
the terraform plan command. For more information on those options, run:
-lock=true Lock the state file when locking is supported. terraform plan -help
-lock-timeout=0s Duration to retry a state lock.
-no-color If specified, output won't contain any color.
-parallelism=n Limit the number of concurrent operations.
Defaults to 10.
-refresh=true Update state prior to checking for differences. This
has no effect if a plan file is given to apply.
-target=resource Resource to target. Operation will be limited to this
resource and its dependencies. This flag can be used
multiple times.
-var 'foo=bar' Set a variable in the Terraform configuration. This
flag can be set multiple times.
-var-file=foo Set variables in the Terraform configuration from
a file. If "terraform.tfvars" or any ".auto.tfvars"
files are present, they will be automatically loaded.
-state, state-out, and -backup are legacy options supported for the local
backend only. For more information, see the local backend's documentation.
` `
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)
} }

View File

@ -1,6 +1,9 @@
package arguments package arguments
import ( import (
"fmt"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
) )
@ -71,3 +74,53 @@ func ParseApply(args []string) (*Apply, tfdiags.Diagnostics) {
return apply, diags return apply, diags
} }
// ParseApplyDestroy is a special case of ParseApply that deals with the
// "terraform destroy" command, which is effectively an alias for
// "terraform apply -destroy".
func ParseApplyDestroy(args []string) (*Apply, tfdiags.Diagnostics) {
apply, diags := ParseApply(args)
// So far ParseApply was using the command line options like -destroy
// and -refresh-only to determine the plan mode. For "terraform destroy"
// we expect neither of those arguments to be set, and so the plan mode
// should currently be set to NormalMode, which we'll replace with
// DestroyMode here. If it's already set to something else then that
// suggests incorrect usage.
switch apply.Operation.PlanMode {
case plans.NormalMode:
// This indicates that the user didn't specify any mode options at
// all, which is correct, although we know from the command that
// they actually intended to use DestroyMode here.
apply.Operation.PlanMode = plans.DestroyMode
case plans.DestroyMode:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid mode option",
"The -destroy option is not valid for \"terraform destroy\", because this command always runs in destroy mode.",
))
case plans.RefreshOnlyMode:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid mode option",
"The -refresh-only option is not valid for \"terraform destroy\".",
))
default:
// This is a non-ideal error message for if we forget to handle a
// newly-handled plan mode in Operation.Parse. Ideally they should all
// have cases above so we can produce better error messages.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid mode option",
fmt.Sprintf("The \"terraform destroy\" command doesn't support %s.", apply.Operation.PlanMode),
))
}
// NOTE: It's also invalid to have apply.PlanPath set in this codepath,
// but we don't check that in here because we'll return a different error
// message depending on whether the given path seems to refer to a saved
// plan file or to a configuration directory. The apply command
// implementation itself therefore handles this situation.
return apply, diags
}

View File

@ -5,7 +5,9 @@ import (
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/plans"
) )
func TestParseApply_basicValid(t *testing.T) { func TestParseApply_basicValid(t *testing.T) {
@ -20,6 +22,13 @@ func TestParseApply_basicValid(t *testing.T) {
InputEnabled: true, InputEnabled: true,
PlanPath: "", PlanPath: "",
ViewType: ViewHuman, ViewType: ViewHuman,
State: &State{Lock: true},
Vars: &Vars{},
Operation: &Operation{
PlanMode: plans.NormalMode,
Parallelism: 10,
Refresh: true,
},
}, },
}, },
"auto-approve, disabled input, and plan path": { "auto-approve, disabled input, and plan path": {
@ -29,22 +38,43 @@ func TestParseApply_basicValid(t *testing.T) {
InputEnabled: false, InputEnabled: false,
PlanPath: "saved.tfplan", PlanPath: "saved.tfplan",
ViewType: ViewHuman, ViewType: ViewHuman,
State: &State{Lock: true},
Vars: &Vars{},
Operation: &Operation{
PlanMode: plans.NormalMode,
Parallelism: 10,
Refresh: true,
},
},
},
"destroy mode": {
[]string{"-destroy"},
&Apply{
AutoApprove: false,
InputEnabled: true,
PlanPath: "",
ViewType: ViewHuman,
State: &State{Lock: true},
Vars: &Vars{},
Operation: &Operation{
PlanMode: plans.DestroyMode,
Parallelism: 10,
Refresh: true,
},
}, },
}, },
} }
cmpOpts := cmpopts.IgnoreUnexported(Operation{}, Vars{}, State{})
for name, tc := range testCases { for name, tc := range testCases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
got, diags := ParseApply(tc.args) got, diags := ParseApply(tc.args)
if len(diags) > 0 { if len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags) t.Fatalf("unexpected diags: %v", diags)
} }
// Ignore the extended arguments for simplicity if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" {
got.State = nil t.Errorf("unexpected result\n%s", diff)
got.Operation = nil
got.Vars = nil
if *got != *tc.want {
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
} }
}) })
} }
@ -175,3 +205,70 @@ func TestParseApply_vars(t *testing.T) {
}) })
} }
} }
func TestParseApplyDestroy_basicValid(t *testing.T) {
testCases := map[string]struct {
args []string
want *Apply
}{
"defaults": {
nil,
&Apply{
AutoApprove: false,
InputEnabled: true,
ViewType: ViewHuman,
State: &State{Lock: true},
Vars: &Vars{},
Operation: &Operation{
PlanMode: plans.DestroyMode,
Parallelism: 10,
Refresh: true,
},
},
},
"auto-approve and disabled input": {
[]string{"-auto-approve", "-input=false"},
&Apply{
AutoApprove: true,
InputEnabled: false,
ViewType: ViewHuman,
State: &State{Lock: true},
Vars: &Vars{},
Operation: &Operation{
PlanMode: plans.DestroyMode,
Parallelism: 10,
Refresh: true,
},
},
},
}
cmpOpts := cmpopts.IgnoreUnexported(Operation{}, Vars{}, State{})
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, diags := ParseApplyDestroy(tc.args)
if len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
}
if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" {
t.Errorf("unexpected result\n%s", diff)
}
})
}
}
func TestParseApplyDestroy_invalid(t *testing.T) {
t.Run("explicit destroy mode", func(t *testing.T) {
got, diags := ParseApplyDestroy([]string{"-destroy"})
if len(diags) == 0 {
t.Fatal("expected diags but got none")
}
if got, want := diags.Err().Error(), "Invalid mode option:"; !strings.Contains(got, want) {
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, want)
}
if got.ViewType != ViewHuman {
t.Fatalf("wrong view type, got %#v, want %#v", got.ViewType, ViewHuman)
}
})
}

View File

@ -8,6 +8,7 @@ import (
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
) )
@ -45,6 +46,11 @@ type State struct {
// Operation describes arguments which are used to configure how a Terraform // Operation describes arguments which are used to configure how a Terraform
// operation such as a plan or apply executes. // operation such as a plan or apply executes.
type Operation struct { type Operation struct {
// PlanMode selects one of the mutually-exclusive planning modes that
// decides the overall goal of a plan operation. This field is relevant
// only for an operation that produces a plan.
PlanMode plans.Mode
// Parallelism is the limit Terraform places on total parallel operations // Parallelism is the limit Terraform places on total parallel operations
// as it walks the dependency graph. // as it walks the dependency graph.
Parallelism int Parallelism int
@ -57,7 +63,11 @@ type Operation struct {
// their dependencies. // their dependencies.
Targets []addrs.Targetable Targets []addrs.Targetable
// These private fields are used only temporarily during decoding. Use
// method Parse to populate the exported fields from these, validating
// the raw values in the process.
targetsRaw []string targetsRaw []string
destroyRaw bool
} }
// Parse must be called on Operation after initial flag parse. This processes // Parse must be called on Operation after initial flag parse. This processes
@ -92,6 +102,15 @@ func (o *Operation) Parse() tfdiags.Diagnostics {
o.Targets = append(o.Targets, target.Subject) o.Targets = append(o.Targets, target.Subject)
} }
// If you add a new possible value for o.PlanMode here, consider also
// adding a specialized error message for it in ParseApplyDestroy.
switch {
case o.destroyRaw:
o.PlanMode = plans.DestroyMode
default:
o.PlanMode = plans.NormalMode
}
return diags return diags
} }
@ -140,6 +159,7 @@ func extendedFlagSet(name string, state *State, operation *Operation, vars *Vars
if operation != nil { if operation != nil {
f.IntVar(&operation.Parallelism, "parallelism", DefaultParallelism, "parallelism") f.IntVar(&operation.Parallelism, "parallelism", DefaultParallelism, "parallelism")
f.BoolVar(&operation.Refresh, "refresh", true, "refresh") f.BoolVar(&operation.Refresh, "refresh", true, "refresh")
f.BoolVar(&operation.destroyRaw, "destroy", false, "destroy")
f.Var((*flagStringSlice)(&operation.targetsRaw), "target", "target") f.Var((*flagStringSlice)(&operation.targetsRaw), "target", "target")
} }

View File

@ -11,9 +11,6 @@ type Plan struct {
Operation *Operation Operation *Operation
Vars *Vars Vars *Vars
// Destroy can be set to generate a plan to destroy all infrastructure.
Destroy bool
// DetailedExitCode enables different exit codes for error, success with // DetailedExitCode enables different exit codes for error, success with
// changes, and success with no changes. // changes, and success with no changes.
DetailedExitCode bool DetailedExitCode bool
@ -41,7 +38,6 @@ func ParsePlan(args []string) (*Plan, tfdiags.Diagnostics) {
} }
cmdFlags := extendedFlagSet("plan", plan.State, plan.Operation, plan.Vars) cmdFlags := extendedFlagSet("plan", plan.State, plan.Operation, plan.Vars)
cmdFlags.BoolVar(&plan.Destroy, "destroy", false, "destroy")
cmdFlags.BoolVar(&plan.DetailedExitCode, "detailed-exitcode", false, "detailed-exitcode") cmdFlags.BoolVar(&plan.DetailedExitCode, "detailed-exitcode", false, "detailed-exitcode")
cmdFlags.BoolVar(&plan.InputEnabled, "input", true, "input") cmdFlags.BoolVar(&plan.InputEnabled, "input", true, "input")
cmdFlags.StringVar(&plan.OutPath, "out", "", "out") cmdFlags.StringVar(&plan.OutPath, "out", "", "out")

View File

@ -5,7 +5,9 @@ import (
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/plans"
) )
func TestParsePlan_basicValid(t *testing.T) { func TestParsePlan_basicValid(t *testing.T) {
@ -16,37 +18,47 @@ func TestParsePlan_basicValid(t *testing.T) {
"defaults": { "defaults": {
nil, nil,
&Plan{ &Plan{
Destroy: false,
DetailedExitCode: false, DetailedExitCode: false,
InputEnabled: true, InputEnabled: true,
OutPath: "", OutPath: "",
ViewType: ViewHuman, ViewType: ViewHuman,
State: &State{Lock: true},
Vars: &Vars{},
Operation: &Operation{
PlanMode: plans.NormalMode,
Parallelism: 10,
Refresh: true,
},
}, },
}, },
"setting all options": { "setting all options": {
[]string{"-destroy", "-detailed-exitcode", "-input=false", "-out=saved.tfplan"}, []string{"-destroy", "-detailed-exitcode", "-input=false", "-out=saved.tfplan"},
&Plan{ &Plan{
Destroy: true,
DetailedExitCode: true, DetailedExitCode: true,
InputEnabled: false, InputEnabled: false,
OutPath: "saved.tfplan", OutPath: "saved.tfplan",
ViewType: ViewHuman, ViewType: ViewHuman,
State: &State{Lock: true},
Vars: &Vars{},
Operation: &Operation{
PlanMode: plans.DestroyMode,
Parallelism: 10,
Refresh: true,
},
}, },
}, },
} }
cmpOpts := cmpopts.IgnoreUnexported(Operation{}, Vars{}, State{})
for name, tc := range testCases { for name, tc := range testCases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
got, diags := ParsePlan(tc.args) got, diags := ParsePlan(tc.args)
if len(diags) > 0 { if len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags) t.Fatalf("unexpected diags: %v", diags)
} }
// Ignore the extended arguments for simplicity if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" {
got.State = nil t.Errorf("unexpected result\n%s", diff)
got.Operation = nil
got.Vars = nil
if *got != *tc.want {
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
} }
}) })
} }

View File

@ -71,7 +71,7 @@ func (c *PlanCommand) Run(rawArgs []string) int {
} }
// Build the operation request // Build the operation request
opReq, opDiags := c.OperationRequest(be, view, args.Operation, args.Destroy, args.OutPath) opReq, opDiags := c.OperationRequest(be, view, args.Operation, args.OutPath)
diags = diags.Append(opDiags) diags = diags.Append(opDiags)
if diags.HasErrors() { if diags.HasErrors() {
view.Diagnostics(diags) view.Diagnostics(diags)
@ -137,7 +137,6 @@ func (c *PlanCommand) OperationRequest(
be backend.Enhanced, be backend.Enhanced,
view views.Plan, view views.Plan,
args *arguments.Operation, args *arguments.Operation,
destroy bool,
planOutPath string, planOutPath string,
) (*backend.Operation, tfdiags.Diagnostics) { ) (*backend.Operation, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
@ -145,7 +144,7 @@ func (c *PlanCommand) OperationRequest(
// Build the operation // Build the operation
opReq := c.Operation(be) opReq := c.Operation(be)
opReq.ConfigDir = "." opReq.ConfigDir = "."
opReq.Destroy = destroy opReq.PlanMode = args.PlanMode
opReq.Hooks = view.Hooks() opReq.Hooks = view.Hooks()
opReq.PlanRefresh = args.Refresh opReq.PlanRefresh = args.Refresh
opReq.PlanOutPath = planOutPath opReq.PlanOutPath = planOutPath
@ -196,15 +195,34 @@ Usage: terraform [global options] plan [options]
You can optionally save the plan to a file, which you can then pass to You can optionally save the plan to a file, which you can then pass to
the "apply" command to perform exactly the actions described in the plan. the "apply" command to perform exactly the actions described in the plan.
Options: Plan Customization Options:
The following options customize how Terraform will produce its plan. You
can also use these options when you run "terraform apply" without passing
it a saved plan, in order to plan and apply in a single command.
-destroy If set, a plan will be generated to destroy all resources
managed by the given configuration and state.
-refresh=true Update state prior to checking for differences.
-target=resource Resource to target. Operation will be limited to this
resource and its dependencies. This flag can be used
multiple times.
-var 'foo=bar' Set a variable in the Terraform configuration. This
flag can be set multiple times.
-var-file=foo Set variables in the Terraform configuration from
a file. If "terraform.tfvars" or any ".auto.tfvars"
files are present, they will be automatically loaded.
Other Options:
-compact-warnings If Terraform produces any warnings that are not -compact-warnings If Terraform produces any warnings that are not
accompanied by errors, show them in a more compact form accompanied by errors, show them in a more compact form
that includes only the summary messages. that includes only the summary messages.
-destroy If set, a plan will be generated to destroy all resources
managed by the given configuration and state.
-detailed-exitcode Return detailed exit codes when the command exits. This -detailed-exitcode Return detailed exit codes when the command exits. This
will change the meaning of exit codes to: will change the meaning of exit codes to:
0 - Succeeded, diff is empty (no changes) 0 - Succeeded, diff is empty (no changes)
@ -224,21 +242,8 @@ Options:
-parallelism=n Limit the number of concurrent operations. Defaults to 10. -parallelism=n Limit the number of concurrent operations. Defaults to 10.
-refresh=true Update state prior to checking for differences.
-state=statefile A legacy option used for the local backend only. See the -state=statefile A legacy option used for the local backend only. See the
local backend's documentation for more information. local backend's documentation for more information.
-target=resource Resource to target. Operation will be limited to this
resource and its dependencies. This flag can be used
multiple times.
-var 'foo=bar' Set a variable in the Terraform configuration. This
flag can be set multiple times.
-var-file=foo Set variables in the Terraform configuration from
a file. If "terraform.tfvars" or any ".auto.tfvars"
files are present, they will be automatically loaded.
` `
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)
} }

View File

@ -18,7 +18,7 @@ type Operation interface {
Interrupted() Interrupted()
FatalInterrupt() FatalInterrupt()
Stopping() Stopping()
Cancelled(destroy bool) Cancelled(planMode plans.Mode)
EmergencyDumpState(stateFile *statefile.File) error EmergencyDumpState(stateFile *statefile.File) error
@ -75,10 +75,11 @@ func (v *OperationHuman) Stopping() {
v.view.streams.Println("Stopping operation...") v.view.streams.Println("Stopping operation...")
} }
func (v *OperationHuman) Cancelled(destroy bool) { func (v *OperationHuman) Cancelled(planMode plans.Mode) {
if destroy { switch planMode {
case plans.DestroyMode:
v.view.streams.Println("Destroy cancelled.") v.view.streams.Println("Destroy cancelled.")
} else { default:
v.view.streams.Println("Apply cancelled.") v.view.streams.Println("Apply cancelled.")
} }
} }

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform/command/arguments" "github.com/hashicorp/terraform/command/arguments"
"github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/states/statefile" "github.com/hashicorp/terraform/states/statefile"
) )
@ -24,16 +25,16 @@ func TestOperation_stopping(t *testing.T) {
func TestOperation_cancelled(t *testing.T) { func TestOperation_cancelled(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
destroy bool planMode plans.Mode
want string want string
}{ }{
"apply": { "apply": {
destroy: false, planMode: plans.NormalMode,
want: "Apply cancelled.\n", want: "Apply cancelled.\n",
}, },
"destroy": { "destroy": {
destroy: true, planMode: plans.DestroyMode,
want: "Destroy cancelled.\n", want: "Destroy cancelled.\n",
}, },
} }
for name, tc := range testCases { for name, tc := range testCases {
@ -41,7 +42,7 @@ func TestOperation_cancelled(t *testing.T) {
streams, done := terminal.StreamsForTesting(t) streams, done := terminal.StreamsForTesting(t)
v := NewOperation(arguments.ViewHuman, false, NewView(streams)) v := NewOperation(arguments.ViewHuman, false, NewView(streams))
v.Cancelled(tc.destroy) v.Cancelled(tc.planMode)
if got, want := done(t).Stdout(), tc.want; got != want { if got, want := done(t).Stdout(), tc.want; got != want {
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want) t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)