mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 01:41:48 -06:00
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:
parent
c6a7d080d9
commit
89f986ded6
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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{
|
||||
|
@ -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."
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user