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
// behavior of the operation.
PlanMode plans.Mode
AutoApprove bool
Destroy bool
Parallelism int
Targets []addrs.Targetable
Variables map[string]UnparsedVariableValue

View File

@ -8,6 +8,7 @@ import (
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/views"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/states/statefile"
"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
// 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(
tfdiags.Error,
"No configuration files",
@ -76,7 +77,7 @@ func (b *Local) opApply(
mustConfirm := hasUI && !op.AutoApprove && !trivialPlan
if mustConfirm {
var desc, query string
if op.Destroy {
if op.PlanMode == plans.DestroyMode {
if op.Workspace != "default" {
query = "Do you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
} else {
@ -116,7 +117,7 @@ func (b *Local) opApply(
return
}
if v != "yes" {
op.View.Cancelled(op.Destroy)
op.View.Cancelled(op.PlanMode)
runningOp.Result = backend.OperationFailure
return
}

View File

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

View File

@ -10,7 +10,6 @@ import (
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/plans/planfile"
"github.com/hashicorp/terraform/states/statemgr"
"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
switch {
case op.Destroy:
opts.PlanMode = plans.DestroyMode
default:
opts.PlanMode = plans.NormalMode
}
opts.PlanMode = op.PlanMode
opts.Targets = op.Targets
opts.UIInput = op.UIIn
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.
if !op.Destroy && !op.HasConfig() {
if op.PlanMode != plans.DestroyMode && !op.HasConfig() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No configuration files",

View File

@ -544,7 +544,7 @@ func TestLocal_planDestroy(t *testing.T) {
op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
op.Destroy = true
op.PlanMode = plans.DestroyMode
op.PlanRefresh = true
op.PlanOutPath = planPath
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")
defer configCleanup()
op.Destroy = true
op.PlanMode = plans.DestroyMode
op.PlanRefresh = true
op.PlanOutPath = planPath
cfg := cty.ObjectVal(map[string]cty.Value{

View File

@ -10,6 +10,7 @@ import (
tfe "github.com/hashicorp/go-tfe"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/terraform"
"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(
tfdiags.Error,
"No configuration files found",
@ -152,10 +153,12 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
if r.Actions.IsDiscardable {
err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{})
if err != nil {
if op.Destroy {
switch op.PlanMode {
case plans.DestroyMode:
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(
@ -176,7 +179,7 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
if mustConfirm {
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.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" +
"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/internal/initwd"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/plans/planfile"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform"
@ -968,7 +969,7 @@ func TestRemote_applyDestroy(t *testing.T) {
"approve": "yes",
})
op.Destroy = true
op.PlanMode = plans.DestroyMode
op.UIIn = input
op.UIOut = b.CLI
op.Workspace = backend.DefaultStateName
@ -1014,7 +1015,7 @@ func TestRemote_applyDestroyNoConfig(t *testing.T) {
defer configCleanup()
defer done(t)
op.Destroy = true
op.PlanMode = plans.DestroyMode
op.UIIn = input
op.UIOut = b.CLI
op.Workspace = backend.DefaultStateName

View File

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

View File

@ -12,7 +12,6 @@ import (
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform"
"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
switch {
case op.Destroy:
opts.PlanMode = plans.DestroyMode
default:
opts.PlanMode = plans.NormalMode
}
opts.PlanMode = op.PlanMode
opts.Targets = op.Targets
opts.UIInput = op.UIIn

View File

@ -17,6 +17,7 @@ import (
tfe "github.com/hashicorp/go-tfe"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/plans"
"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(
tfdiags.Error,
"No configuration files found",
@ -238,12 +239,26 @@ in order to capture the filesystem context the remote workspace expects:
}
runOptions := tfe.RunCreateOptions{
IsDestroy: tfe.Bool(op.Destroy),
Message: tfe.String(queueMessage),
ConfigurationVersion: cv,
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 {
runOptions.TargetAddrs = make([]string, 0, len(op.Targets))
for _, addr := range op.Targets {

View File

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

View File

@ -23,6 +23,8 @@ type ApplyCommand struct {
}
func (c *ApplyCommand) Run(rawArgs []string) int {
var diags tfdiags.Diagnostics
// Parse and apply global view arguments
common, rawArgs := arguments.ParseView(rawArgs)
c.View.Configure(common)
@ -33,7 +35,13 @@ func (c *ApplyCommand) Run(rawArgs []string) int {
c.Meta.Color = c.Meta.color
// 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
// diagnostics according to the desired view
@ -253,7 +261,7 @@ func (c *ApplyCommand) OperationRequest(
opReq := c.Operation(be)
opReq.AutoApprove = autoApprove
opReq.ConfigDir = "."
opReq.Destroy = c.Destroy
opReq.PlanMode = args.PlanMode
opReq.Hooks = view.Hooks()
opReq.PlanFile = planFile
opReq.PlanRefresh = args.Refresh
@ -345,9 +353,6 @@ Options:
-parallelism=n Limit the number of parallel resource 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.
-state=path Path to read and save state (unless state-out
is specified). Defaults to "terraform.tfstate".
@ -355,18 +360,10 @@ Options:
"-state". This can be used to preserve the old
state.
-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.
If you don't provide a saved plan file then this command will also accept
all of the plan-customization options accepted by the terraform plan command.
For more information on those options, run:
terraform plan -help
`
return strings.TrimSpace(helpText)
}
@ -377,35 +374,12 @@ Usage: terraform [global options] destroy [options]
Destroy Terraform-managed infrastructure.
Options:
This command is a convenience alias for:
terraform apply -destroy
-auto-approve Skip interactive approval before destroying.
-lock=true Lock the state file when locking is supported.
-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.
This command also accepts many of the plan-customization options accepted by
the terraform plan command. For more information on those options, run:
terraform plan -help
`
return strings.TrimSpace(helpText)
}

View File

@ -1,6 +1,9 @@
package arguments
import (
"fmt"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/tfdiags"
)
@ -71,3 +74,53 @@ func ParseApply(args []string) (*Apply, tfdiags.Diagnostics) {
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"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/plans"
)
func TestParseApply_basicValid(t *testing.T) {
@ -20,6 +22,13 @@ func TestParseApply_basicValid(t *testing.T) {
InputEnabled: true,
PlanPath: "",
ViewType: ViewHuman,
State: &State{Lock: true},
Vars: &Vars{},
Operation: &Operation{
PlanMode: plans.NormalMode,
Parallelism: 10,
Refresh: true,
},
},
},
"auto-approve, disabled input, and plan path": {
@ -29,22 +38,43 @@ func TestParseApply_basicValid(t *testing.T) {
InputEnabled: false,
PlanPath: "saved.tfplan",
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 {
t.Run(name, func(t *testing.T) {
got, diags := ParseApply(tc.args)
if len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
}
// Ignore the extended arguments for simplicity
got.State = nil
got.Operation = nil
got.Vars = nil
if *got != *tc.want {
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" {
t.Errorf("unexpected result\n%s", diff)
}
})
}
@ -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/hclsyntax"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/tfdiags"
)
@ -45,6 +46,11 @@ type State struct {
// Operation describes arguments which are used to configure how a Terraform
// operation such as a plan or apply executes.
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
// as it walks the dependency graph.
Parallelism int
@ -57,7 +63,11 @@ type Operation struct {
// their dependencies.
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
destroyRaw bool
}
// 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)
}
// 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
}
@ -140,6 +159,7 @@ func extendedFlagSet(name string, state *State, operation *Operation, vars *Vars
if operation != nil {
f.IntVar(&operation.Parallelism, "parallelism", DefaultParallelism, "parallelism")
f.BoolVar(&operation.Refresh, "refresh", true, "refresh")
f.BoolVar(&operation.destroyRaw, "destroy", false, "destroy")
f.Var((*flagStringSlice)(&operation.targetsRaw), "target", "target")
}

View File

@ -11,9 +11,6 @@ type Plan struct {
Operation *Operation
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
// changes, and success with no changes.
DetailedExitCode bool
@ -41,7 +38,6 @@ func ParsePlan(args []string) (*Plan, tfdiags.Diagnostics) {
}
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.InputEnabled, "input", true, "input")
cmdFlags.StringVar(&plan.OutPath, "out", "", "out")

View File

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

View File

@ -71,7 +71,7 @@ func (c *PlanCommand) Run(rawArgs []string) int {
}
// 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)
if diags.HasErrors() {
view.Diagnostics(diags)
@ -137,7 +137,6 @@ func (c *PlanCommand) OperationRequest(
be backend.Enhanced,
view views.Plan,
args *arguments.Operation,
destroy bool,
planOutPath string,
) (*backend.Operation, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
@ -145,7 +144,7 @@ func (c *PlanCommand) OperationRequest(
// Build the operation
opReq := c.Operation(be)
opReq.ConfigDir = "."
opReq.Destroy = destroy
opReq.PlanMode = args.PlanMode
opReq.Hooks = view.Hooks()
opReq.PlanRefresh = args.Refresh
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
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
accompanied by errors, show them in a more compact form
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
will change the meaning of exit codes to:
0 - Succeeded, diff is empty (no changes)
@ -224,21 +242,8 @@ Options:
-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
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)
}

View File

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

View File

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