Merge pull request #27841 from hashicorp/alisdair/command-views-apply
cli: Migrate apply to command views
This commit is contained in:
commit
56b756cfd9
321
command/apply.go
321
command/apply.go
|
@ -8,7 +8,6 @@ import (
|
||||||
"github.com/hashicorp/terraform/command/arguments"
|
"github.com/hashicorp/terraform/command/arguments"
|
||||||
"github.com/hashicorp/terraform/command/views"
|
"github.com/hashicorp/terraform/command/views"
|
||||||
"github.com/hashicorp/terraform/plans/planfile"
|
"github.com/hashicorp/terraform/plans/planfile"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
"github.com/hashicorp/terraform/tfdiags"
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,105 +21,177 @@ type ApplyCommand struct {
|
||||||
Destroy bool
|
Destroy bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ApplyCommand) Run(args []string) int {
|
func (c *ApplyCommand) Run(rawArgs []string) int {
|
||||||
var refresh, autoApprove bool
|
// Parse and apply global view arguments
|
||||||
args = c.Meta.process(args)
|
common, rawArgs := arguments.ParseView(rawArgs)
|
||||||
cmdName := "apply"
|
c.View.Configure(common)
|
||||||
if c.Destroy {
|
|
||||||
cmdName = "destroy"
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdFlags := c.Meta.extendedFlagSet(cmdName)
|
// Parse and validate flags
|
||||||
cmdFlags.BoolVar(&autoApprove, "auto-approve", false, "skip interactive approval of plan before applying")
|
args, diags := arguments.ParseApply(rawArgs)
|
||||||
cmdFlags.BoolVar(&refresh, "refresh", true, "refresh")
|
|
||||||
cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism")
|
// Instantiate the view, even if there are flag errors, so that we render
|
||||||
cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
|
// diagnostics according to the desired view
|
||||||
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
|
var view views.Apply
|
||||||
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
view = views.NewApply(args.ViewType, c.Destroy, c.RunningInAutomation, c.View)
|
||||||
cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state")
|
|
||||||
cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout")
|
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
diags := c.parseTargetFlags()
|
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
c.showDiagnostics(diags)
|
view.Diagnostics(diags)
|
||||||
return 1
|
view.HelpPrompt()
|
||||||
}
|
|
||||||
|
|
||||||
args = cmdFlags.Args()
|
|
||||||
var planPath string
|
|
||||||
if len(args) > 0 {
|
|
||||||
planPath = args[0]
|
|
||||||
args = args[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
configPath, err := ModulePath(args)
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(err.Error())
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for user-supplied plugin path
|
// Check for user-supplied plugin path
|
||||||
|
var err error
|
||||||
if c.pluginPath, err = c.loadPluginPath(); err != nil {
|
if c.pluginPath, err = c.loadPluginPath(); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err))
|
diags = diags.Append(err)
|
||||||
|
view.Diagnostics(diags)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to load plan if path is specified
|
// Attempt to load the plan file, if specified
|
||||||
|
planFile, diags := c.LoadPlanFile(args.PlanPath)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
view.Diagnostics(diags)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for invalid combination of plan file and variable overrides
|
||||||
|
if planFile != nil && !args.Vars.Empty() {
|
||||||
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
|
tfdiags.Error,
|
||||||
|
"Can't set variables when applying a saved plan",
|
||||||
|
"The -var and -var-file options cannot be used when applying a saved plan file, because a saved plan includes the variable values that were set when it was created.",
|
||||||
|
))
|
||||||
|
view.Diagnostics(diags)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: the -input flag value is needed to initialize the backend and the
|
||||||
|
// operation, but there is no clear path to pass this value down, so we
|
||||||
|
// continue to mutate the Meta object state for now.
|
||||||
|
c.Meta.input = args.InputEnabled
|
||||||
|
|
||||||
|
// FIXME: the -parallelism flag is used to control the concurrency of
|
||||||
|
// Terraform operations. At the moment, this value is used both to
|
||||||
|
// initialize the backend via the ContextOpts field inside CLIOpts, and to
|
||||||
|
// set a largely unused field on the Operation request. Again, there is no
|
||||||
|
// clear path to pass this value down, so we continue to mutate the Meta
|
||||||
|
// object state for now.
|
||||||
|
c.Meta.parallelism = args.Operation.Parallelism
|
||||||
|
|
||||||
|
// Prepare the backend, passing the plan file if present, and the
|
||||||
|
// backend-specific arguments
|
||||||
|
be, beDiags := c.PrepareBackend(planFile, args.State)
|
||||||
|
diags = diags.Append(beDiags)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
view.Diagnostics(diags)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the operation request
|
||||||
|
opReq, opDiags := c.OperationRequest(be, view, planFile, args.Operation)
|
||||||
|
diags = diags.Append(opDiags)
|
||||||
|
|
||||||
|
// Collect variable value and add them to the operation request
|
||||||
|
diags = diags.Append(c.GatherVariables(opReq, args.Vars))
|
||||||
|
|
||||||
|
// Before we delegate to the backend, we'll print any warning diagnostics
|
||||||
|
// we've accumulated here, since the backend will start fresh with its own
|
||||||
|
// diagnostics.
|
||||||
|
view.Diagnostics(diags)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
diags = nil
|
||||||
|
|
||||||
|
// Run the operation
|
||||||
|
op, err := c.RunOperation(be, opReq)
|
||||||
|
if err != nil {
|
||||||
|
diags = diags.Append(err)
|
||||||
|
view.Diagnostics(diags)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if op.Result != backend.OperationSuccess {
|
||||||
|
return op.Result.ExitStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Render the resource count and outputs
|
||||||
|
view.ResourceCount(args.State.StateOutPath)
|
||||||
|
if !c.Destroy && op.State != nil {
|
||||||
|
view.Outputs(op.State.RootModule().OutputValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.Diagnostics(diags)
|
||||||
|
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ApplyCommand) LoadPlanFile(path string) (*planfile.Reader, tfdiags.Diagnostics) {
|
||||||
var planFile *planfile.Reader
|
var planFile *planfile.Reader
|
||||||
if planPath != "" {
|
var diags tfdiags.Diagnostics
|
||||||
planFile, err = c.PlanFile(planPath)
|
|
||||||
|
// Try to load plan if path is specified
|
||||||
|
if path != "" {
|
||||||
|
var err error
|
||||||
|
planFile, err = c.PlanFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(err.Error())
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
return 1
|
tfdiags.Error,
|
||||||
|
fmt.Sprintf("Failed to load %q as a plan file", path),
|
||||||
|
fmt.Sprintf("Error: %s", err),
|
||||||
|
))
|
||||||
|
return nil, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the path doesn't look like a plan, both planFile and err will be
|
// If the path doesn't look like a plan, both planFile and err will be
|
||||||
// nil. In that case, the user is probably trying to use the positional
|
// nil. In that case, the user is probably trying to use the positional
|
||||||
// argument to specify a configuration path. Point them at -chdir.
|
// argument to specify a configuration path. Point them at -chdir.
|
||||||
if planFile == nil {
|
if planFile == nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to load %q as a plan file. Did you mean to use -chdir?", planPath))
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
return 1
|
tfdiags.Error,
|
||||||
|
fmt.Sprintf("Failed to load %q as a plan file", path),
|
||||||
|
"The specified path is a directory, not a plan file. You can use the global -chdir flag to use this directory as the configuration root.",
|
||||||
|
))
|
||||||
|
return nil, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we successfully loaded a plan but this is a destroy operation,
|
// If we successfully loaded a plan but this is a destroy operation,
|
||||||
// explain that this is not supported.
|
// explain that this is not supported.
|
||||||
if c.Destroy {
|
if c.Destroy {
|
||||||
c.Ui.Error("Destroy can't be called with a plan file.")
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if planFile != nil {
|
|
||||||
// Reset the config path for backend loading
|
|
||||||
configPath = ""
|
|
||||||
|
|
||||||
if !c.variableArgs.Empty() {
|
|
||||||
diags = diags.Append(tfdiags.Sourceless(
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
"Can't set variables when applying a saved plan",
|
"Destroy can't be called with a plan file",
|
||||||
"The -var and -var-file options cannot be used when applying a saved plan file, because a saved plan includes the variable values that were set when it was created.",
|
fmt.Sprintf("If this plan was created using plan -destroy, apply it using:\n terraform apply %q", path),
|
||||||
))
|
))
|
||||||
c.showDiagnostics(diags)
|
return nil, diags
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up our count hook that keeps track of resource changes
|
return planFile, diags
|
||||||
countHook := new(CountHook)
|
}
|
||||||
|
|
||||||
|
func (c *ApplyCommand) PrepareBackend(planFile *planfile.Reader, args *arguments.State) (backend.Enhanced, tfdiags.Diagnostics) {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
|
// FIXME: we need to apply the state arguments to the meta object here
|
||||||
|
// because they are later used when initializing the backend. Carving a
|
||||||
|
// path to pass these arguments to the functions that need them is
|
||||||
|
// difficult but would make their use easier to understand.
|
||||||
|
c.Meta.applyStateArguments(args)
|
||||||
|
|
||||||
// Load the backend
|
// Load the backend
|
||||||
var be backend.Enhanced
|
var be backend.Enhanced
|
||||||
var beDiags tfdiags.Diagnostics
|
var beDiags tfdiags.Diagnostics
|
||||||
if planFile == nil {
|
if planFile == nil {
|
||||||
backendConfig, configDiags := c.loadBackendConfig(configPath)
|
backendConfig, configDiags := c.loadBackendConfig(".")
|
||||||
diags = diags.Append(configDiags)
|
diags = diags.Append(configDiags)
|
||||||
if configDiags.HasErrors() {
|
if configDiags.HasErrors() {
|
||||||
c.showDiagnostics(diags)
|
return nil, diags
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
be, beDiags = c.Backend(&BackendOpts{
|
be, beDiags = c.Backend(&BackendOpts{
|
||||||
|
@ -134,8 +205,7 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
"Failed to read plan from plan file",
|
"Failed to read plan from plan file",
|
||||||
fmt.Sprintf("Cannot read the plan from the given plan file: %s.", err),
|
fmt.Sprintf("Cannot read the plan from the given plan file: %s.", err),
|
||||||
))
|
))
|
||||||
c.showDiagnostics(diags)
|
return nil, diags
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
if plan.Backend.Config == nil {
|
if plan.Backend.Config == nil {
|
||||||
// Should never happen; always indicates a bug in the creation of the plan file
|
// Should never happen; always indicates a bug in the creation of the plan file
|
||||||
|
@ -144,103 +214,80 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
"Failed to read plan from plan file",
|
"Failed to read plan from plan file",
|
||||||
"The given plan file does not have a valid backend configuration. This is a bug in the Terraform command that generated this plan file.",
|
"The given plan file does not have a valid backend configuration. This is a bug in the Terraform command that generated this plan file.",
|
||||||
))
|
))
|
||||||
c.showDiagnostics(diags)
|
return nil, diags
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
be, beDiags = c.BackendForPlan(plan.Backend)
|
be, beDiags = c.BackendForPlan(plan.Backend)
|
||||||
}
|
}
|
||||||
|
|
||||||
diags = diags.Append(beDiags)
|
diags = diags.Append(beDiags)
|
||||||
if beDiags.HasErrors() {
|
if beDiags.HasErrors() {
|
||||||
c.showDiagnostics(diags)
|
return nil, diags
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
return be, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ApplyCommand) OperationRequest(be backend.Enhanced, view views.Apply, planFile *planfile.Reader, args *arguments.Operation,
|
||||||
|
) (*backend.Operation, tfdiags.Diagnostics) {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
// Applying changes with dev overrides in effect could make it impossible
|
// Applying changes with dev overrides in effect could make it impossible
|
||||||
// to switch back to a release version if the schema isn't compatible,
|
// to switch back to a release version if the schema isn't compatible,
|
||||||
// so we'll warn about it.
|
// so we'll warn about it.
|
||||||
diags = diags.Append(c.providerDevOverrideRuntimeWarnings())
|
diags = diags.Append(c.providerDevOverrideRuntimeWarnings())
|
||||||
|
|
||||||
// Before we delegate to the backend, we'll print any warning diagnostics
|
|
||||||
// we've accumulated here, since the backend will start fresh with its own
|
|
||||||
// diagnostics.
|
|
||||||
c.showDiagnostics(diags)
|
|
||||||
diags = nil
|
|
||||||
|
|
||||||
// Build the operation
|
// Build the operation
|
||||||
opReq := c.Operation(be)
|
opReq := c.Operation(be)
|
||||||
opReq.AutoApprove = autoApprove
|
opReq.AutoApprove = args.AutoApprove
|
||||||
opReq.ConfigDir = configPath
|
opReq.ConfigDir = "."
|
||||||
opReq.Destroy = c.Destroy
|
opReq.Destroy = c.Destroy
|
||||||
opReq.Hooks = []terraform.Hook{countHook, c.uiHook()}
|
opReq.Hooks = view.Hooks()
|
||||||
opReq.PlanFile = planFile
|
opReq.PlanFile = planFile
|
||||||
opReq.PlanRefresh = refresh
|
opReq.PlanRefresh = args.Refresh
|
||||||
opReq.ShowDiagnostics = c.showDiagnostics
|
opReq.Targets = args.Targets
|
||||||
opReq.Type = backend.OperationTypeApply
|
opReq.Type = backend.OperationTypeApply
|
||||||
opReq.View = views.NewOperation(arguments.ViewHuman, c.RunningInAutomation, c.View)
|
opReq.View = view.Operation()
|
||||||
|
|
||||||
|
// FIXME: To allow errors to be easily rendered, the showDiagnostics method
|
||||||
|
// accepts ...interface{}. The backend no longer needs this, as only
|
||||||
|
// tfdiags.Diagnostics values are used. Once we have migrated plan and refresh
|
||||||
|
// to use views, we can remove ShowDiagnostics and update the backend code to
|
||||||
|
// call view.Diagnostics() instead.
|
||||||
|
opReq.ShowDiagnostics = func(vals ...interface{}) {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
diags = diags.Append(vals...)
|
||||||
|
view.Diagnostics(diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
opReq.ConfigLoader, err = c.initConfigLoader()
|
opReq.ConfigLoader, err = c.initConfigLoader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.showDiagnostics(err)
|
diags = diags.Append(fmt.Errorf("Failed to initialize config loader: %s", err))
|
||||||
return 1
|
return nil, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
return opReq, diags
|
||||||
var moreDiags tfdiags.Diagnostics
|
}
|
||||||
opReq.Variables, moreDiags = c.collectVariableValues()
|
|
||||||
diags = diags.Append(moreDiags)
|
|
||||||
if moreDiags.HasErrors() {
|
|
||||||
c.showDiagnostics(diags)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
op, err := c.RunOperation(be, opReq)
|
func (c *ApplyCommand) GatherVariables(opReq *backend.Operation, args *arguments.Vars) tfdiags.Diagnostics {
|
||||||
if err != nil {
|
var diags tfdiags.Diagnostics
|
||||||
c.showDiagnostics(err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if op.Result != backend.OperationSuccess {
|
// FIXME the arguments package currently trivially gathers variable related
|
||||||
return op.Result.ExitStatus()
|
// arguments in a heterogenous slice, in order to minimize the number of
|
||||||
}
|
// code paths gathering variables during the transition to this structure.
|
||||||
|
// Once all commands that gather variables have been converted to this
|
||||||
|
// structure, we could move the variable gathering code to the arguments
|
||||||
|
// package directly, removing this shim layer.
|
||||||
|
|
||||||
// Show the count results from the operation
|
varArgs := args.All()
|
||||||
if c.Destroy {
|
items := make([]rawFlag, len(varArgs))
|
||||||
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
for i := range varArgs {
|
||||||
"[reset][bold][green]\n"+
|
items[i].Name = varArgs[i].Name
|
||||||
"Destroy complete! Resources: %d destroyed.",
|
items[i].Value = varArgs[i].Value
|
||||||
countHook.Removed)))
|
|
||||||
} else {
|
|
||||||
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
|
||||||
"[reset][bold][green]\n"+
|
|
||||||
"Apply complete! Resources: %d added, %d changed, %d destroyed.",
|
|
||||||
countHook.Added,
|
|
||||||
countHook.Changed,
|
|
||||||
countHook.Removed)))
|
|
||||||
}
|
}
|
||||||
|
c.Meta.variableArgs = rawFlags{items: &items}
|
||||||
|
opReq.Variables, diags = c.collectVariableValues()
|
||||||
|
|
||||||
// only show the state file help message if the state is local.
|
return diags
|
||||||
if (countHook.Added > 0 || countHook.Changed > 0) && c.Meta.stateOutPath != "" {
|
|
||||||
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
|
||||||
"[reset]\n"+
|
|
||||||
"The state of your infrastructure has been saved to the path\n"+
|
|
||||||
"below. This state is required to modify and destroy your\n"+
|
|
||||||
"infrastructure, so keep it safe. To inspect the complete state\n"+
|
|
||||||
"use the `terraform show` command.\n\n"+
|
|
||||||
"State path: %s",
|
|
||||||
c.Meta.stateOutPath)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.Destroy && op.State != nil {
|
|
||||||
outputValues := op.State.RootModule().OutputValues
|
|
||||||
if len(outputValues) > 0 {
|
|
||||||
c.Ui.Output(c.Colorize().Color("[reset][bold][green]\nOutputs:\n\n"))
|
|
||||||
view := views.NewOutput(arguments.ViewHuman, c.View)
|
|
||||||
view.Output("", outputValues)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return op.Result.ExitStatus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ApplyCommand) Help() string {
|
func (c *ApplyCommand) Help() string {
|
||||||
|
@ -369,7 +416,3 @@ Options:
|
||||||
`
|
`
|
||||||
return strings.TrimSpace(helpText)
|
return strings.TrimSpace(helpText)
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputInterrupt = `Interrupt received.
|
|
||||||
Please wait for Terraform to exit or data loss may occur.
|
|
||||||
Gracefully shutting down...`
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -57,14 +56,11 @@ func TestApply_destroy(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
view, done := testView(t)
|
view, done := testView(t)
|
||||||
defer done(t)
|
|
||||||
c := &ApplyCommand{
|
c := &ApplyCommand{
|
||||||
Destroy: true,
|
Destroy: true,
|
||||||
Meta: Meta{
|
Meta: Meta{
|
||||||
testingOverrides: metaOverridesForProvider(p),
|
testingOverrides: metaOverridesForProvider(p),
|
||||||
Ui: ui,
|
|
||||||
View: view,
|
View: view,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -74,9 +70,11 @@ func TestApply_destroy(t *testing.T) {
|
||||||
"-auto-approve",
|
"-auto-approve",
|
||||||
"-state", statePath,
|
"-state", statePath,
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 0 {
|
code := c.Run(args)
|
||||||
t.Log(ui.OutputWriter.String())
|
output := done(t)
|
||||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
if code != 0 {
|
||||||
|
t.Log(output.Stdout())
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify a new state exists
|
// Verify a new state exists
|
||||||
|
@ -150,15 +148,14 @@ func TestApply_destroyApproveNo(t *testing.T) {
|
||||||
})
|
})
|
||||||
statePath := testStateFile(t, originalState)
|
statePath := testStateFile(t, originalState)
|
||||||
|
|
||||||
// Disable test mode so input would be asked
|
|
||||||
test = false
|
|
||||||
defer func() { test = true }()
|
|
||||||
|
|
||||||
// Answer approval request with "no"
|
|
||||||
defaultInputReader = bytes.NewBufferString("no\n")
|
|
||||||
defaultInputWriter = new(bytes.Buffer)
|
|
||||||
|
|
||||||
p := applyFixtureProvider()
|
p := applyFixtureProvider()
|
||||||
|
|
||||||
|
defer testInputMap(t, map[string]string{
|
||||||
|
"approve": "no",
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Do not use the NewMockUi initializer here, as we want to delay
|
||||||
|
// the call to init until after setting up the input mocks
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
view, done := testView(t)
|
view, done := testView(t)
|
||||||
c := &ApplyCommand{
|
c := &ApplyCommand{
|
||||||
|
@ -173,10 +170,12 @@ func TestApply_destroyApproveNo(t *testing.T) {
|
||||||
args := []string{
|
args := []string{
|
||||||
"-state", statePath,
|
"-state", statePath,
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 1 {
|
code := c.Run(args)
|
||||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
output := done(t)
|
||||||
|
if code != 1 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, output.Stdout())
|
||||||
}
|
}
|
||||||
if got, want := done(t).Stdout(), "Destroy cancelled"; !strings.Contains(got, want) {
|
if got, want := output.Stdout(), "Destroy cancelled"; !strings.Contains(got, want) {
|
||||||
t.Fatalf("expected output to include %q, but was:\n%s", want, got)
|
t.Fatalf("expected output to include %q, but was:\n%s", want, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,20 +197,36 @@ func TestApply_destroyApproveYes(t *testing.T) {
|
||||||
defer os.RemoveAll(td)
|
defer os.RemoveAll(td)
|
||||||
defer testChdir(t, td)()
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
statePath := testTempFile(t)
|
// Create some existing state
|
||||||
|
originalState := states.BuildState(func(s *states.SyncState) {
|
||||||
|
s.SetResourceInstanceCurrent(
|
||||||
|
addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_instance",
|
||||||
|
Name: "foo",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||||
|
&states.ResourceInstanceObjectSrc{
|
||||||
|
AttrsJSON: []byte(`{"id":"bar"}`),
|
||||||
|
Status: states.ObjectReady,
|
||||||
|
},
|
||||||
|
addrs.AbsProviderConfig{
|
||||||
|
Provider: addrs.NewDefaultProvider("test"),
|
||||||
|
Module: addrs.RootModule,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
statePath := testStateFile(t, originalState)
|
||||||
|
|
||||||
p := applyFixtureProvider()
|
p := applyFixtureProvider()
|
||||||
|
|
||||||
// Disable test mode so input would be asked
|
defer testInputMap(t, map[string]string{
|
||||||
test = false
|
"approve": "yes",
|
||||||
defer func() { test = true }()
|
})()
|
||||||
|
|
||||||
// Answer approval request with "yes"
|
|
||||||
defaultInputReader = bytes.NewBufferString("yes\n")
|
|
||||||
defaultInputWriter = new(bytes.Buffer)
|
|
||||||
|
|
||||||
|
// Do not use the NewMockUi initializer here, as we want to delay
|
||||||
|
// the call to init until after setting up the input mocks
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
view, _ := testView(t)
|
view, done := testView(t)
|
||||||
c := &ApplyCommand{
|
c := &ApplyCommand{
|
||||||
Destroy: true,
|
Destroy: true,
|
||||||
Meta: Meta{
|
Meta: Meta{
|
||||||
|
@ -224,8 +239,11 @@ func TestApply_destroyApproveYes(t *testing.T) {
|
||||||
args := []string{
|
args := []string{
|
||||||
"-state", statePath,
|
"-state", statePath,
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 0 {
|
code := c.Run(args)
|
||||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
output := done(t)
|
||||||
|
if code != 0 {
|
||||||
|
t.Log(output.Stdout())
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(statePath); err != nil {
|
if _, err := os.Stat(statePath); err != nil {
|
||||||
|
@ -277,13 +295,11 @@ func TestApply_destroyLockedState(t *testing.T) {
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
view, done := testView(t)
|
||||||
view, _ := testView(t)
|
|
||||||
c := &ApplyCommand{
|
c := &ApplyCommand{
|
||||||
Destroy: true,
|
Destroy: true,
|
||||||
Meta: Meta{
|
Meta: Meta{
|
||||||
testingOverrides: metaOverridesForProvider(p),
|
testingOverrides: metaOverridesForProvider(p),
|
||||||
Ui: ui,
|
|
||||||
View: view,
|
View: view,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -294,13 +310,14 @@ func TestApply_destroyLockedState(t *testing.T) {
|
||||||
"-state", statePath,
|
"-state", statePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
if code := c.Run(args); code == 0 {
|
code := c.Run(args)
|
||||||
t.Fatal("expected error")
|
output := done(t)
|
||||||
|
if code == 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, output.Stdout())
|
||||||
}
|
}
|
||||||
|
|
||||||
output := ui.ErrorWriter.String()
|
if !strings.Contains(output.Stderr(), "lock") {
|
||||||
if !strings.Contains(output, "lock") {
|
t.Fatal("command output does not look like a lock error:", output.Stderr())
|
||||||
t.Fatal("command output does not look like a lock error:", output)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,13 +331,11 @@ func TestApply_destroyPlan(t *testing.T) {
|
||||||
planPath := testPlanFileNoop(t)
|
planPath := testPlanFileNoop(t)
|
||||||
|
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
view, done := testView(t)
|
||||||
view, _ := testView(t)
|
|
||||||
c := &ApplyCommand{
|
c := &ApplyCommand{
|
||||||
Destroy: true,
|
Destroy: true,
|
||||||
Meta: Meta{
|
Meta: Meta{
|
||||||
testingOverrides: metaOverridesForProvider(p),
|
testingOverrides: metaOverridesForProvider(p),
|
||||||
Ui: ui,
|
|
||||||
View: view,
|
View: view,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -329,12 +344,13 @@ func TestApply_destroyPlan(t *testing.T) {
|
||||||
args := []string{
|
args := []string{
|
||||||
planPath,
|
planPath,
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 1 {
|
code := c.Run(args)
|
||||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
output := done(t)
|
||||||
|
if code != 1 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, output.Stdout())
|
||||||
}
|
}
|
||||||
output := ui.ErrorWriter.String()
|
if !strings.Contains(output.Stderr(), "plan file") {
|
||||||
if !strings.Contains(output, "plan file") {
|
t.Fatal("expected command output to refer to plan file, but got:", output.Stderr())
|
||||||
t.Fatal("expected command output to refer to plan file, but got:", output)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -347,13 +363,11 @@ func TestApply_destroyPath(t *testing.T) {
|
||||||
|
|
||||||
p := applyFixtureProvider()
|
p := applyFixtureProvider()
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
view, done := testView(t)
|
||||||
view, _ := testView(t)
|
|
||||||
c := &ApplyCommand{
|
c := &ApplyCommand{
|
||||||
Destroy: true,
|
Destroy: true,
|
||||||
Meta: Meta{
|
Meta: Meta{
|
||||||
testingOverrides: metaOverridesForProvider(p),
|
testingOverrides: metaOverridesForProvider(p),
|
||||||
Ui: ui,
|
|
||||||
View: view,
|
View: view,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -362,12 +376,13 @@ func TestApply_destroyPath(t *testing.T) {
|
||||||
"-auto-approve",
|
"-auto-approve",
|
||||||
testFixturePath("apply"),
|
testFixturePath("apply"),
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 1 {
|
code := c.Run(args)
|
||||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
output := done(t)
|
||||||
|
if code != 1 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, output.Stdout())
|
||||||
}
|
}
|
||||||
output := ui.ErrorWriter.String()
|
if !strings.Contains(output.Stderr(), "-chdir") {
|
||||||
if !strings.Contains(output, "-chdir") {
|
t.Fatal("expected command output to refer to -chdir flag, but got:", output.Stderr())
|
||||||
t.Fatal("expected command output to refer to -chdir flag, but got:", output)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -442,13 +457,11 @@ func TestApply_destroyTargetedDependencies(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
view, done := testView(t)
|
||||||
view, _ := testView(t)
|
|
||||||
c := &ApplyCommand{
|
c := &ApplyCommand{
|
||||||
Destroy: true,
|
Destroy: true,
|
||||||
Meta: Meta{
|
Meta: Meta{
|
||||||
testingOverrides: metaOverridesForProvider(p),
|
testingOverrides: metaOverridesForProvider(p),
|
||||||
Ui: ui,
|
|
||||||
View: view,
|
View: view,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -459,8 +472,11 @@ func TestApply_destroyTargetedDependencies(t *testing.T) {
|
||||||
"-target", "test_instance.foo",
|
"-target", "test_instance.foo",
|
||||||
"-state", statePath,
|
"-state", statePath,
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 0 {
|
code := c.Run(args)
|
||||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
output := done(t)
|
||||||
|
if code != 0 {
|
||||||
|
t.Log(output.Stdout())
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify a new state exists
|
// Verify a new state exists
|
||||||
|
@ -593,13 +609,11 @@ func TestApply_destroyTargeted(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
view, done := testView(t)
|
||||||
view, _ := testView(t)
|
|
||||||
c := &ApplyCommand{
|
c := &ApplyCommand{
|
||||||
Destroy: true,
|
Destroy: true,
|
||||||
Meta: Meta{
|
Meta: Meta{
|
||||||
testingOverrides: metaOverridesForProvider(p),
|
testingOverrides: metaOverridesForProvider(p),
|
||||||
Ui: ui,
|
|
||||||
View: view,
|
View: view,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -610,8 +624,11 @@ func TestApply_destroyTargeted(t *testing.T) {
|
||||||
"-target", "test_load_balancer.foo",
|
"-target", "test_load_balancer.foo",
|
||||||
"-state", statePath,
|
"-state", statePath,
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 0 {
|
code := c.Run(args)
|
||||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
output := done(t)
|
||||||
|
if code != 0 {
|
||||||
|
t.Log(output.Stdout())
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify a new state exists
|
// Verify a new state exists
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,69 @@
|
||||||
|
package arguments
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Apply represents the command-line arguments for the apply command.
|
||||||
|
type Apply struct {
|
||||||
|
// State, Operation, and Vars are the common extended flags
|
||||||
|
State *State
|
||||||
|
Operation *Operation
|
||||||
|
Vars *Vars
|
||||||
|
|
||||||
|
// InputEnabled is used to disable interactive input for unspecified
|
||||||
|
// variable and backend config values. Default is true.
|
||||||
|
InputEnabled bool
|
||||||
|
|
||||||
|
// PlanPath contains an optional path to a stored plan file
|
||||||
|
PlanPath string
|
||||||
|
|
||||||
|
// ViewType specifies which output format to use
|
||||||
|
ViewType ViewType
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseApply processes CLI arguments, returning an Apply value and errors.
|
||||||
|
// If errors are encountered, an Apply value is still returned representing
|
||||||
|
// the best effort interpretation of the arguments.
|
||||||
|
func ParseApply(args []string) (*Apply, tfdiags.Diagnostics) {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
apply := &Apply{
|
||||||
|
State: &State{},
|
||||||
|
Operation: &Operation{},
|
||||||
|
Vars: &Vars{},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdFlags := extendedFlagSet("apply", apply.State, apply.Operation, apply.Vars)
|
||||||
|
cmdFlags.BoolVar(&apply.InputEnabled, "input", true, "input")
|
||||||
|
|
||||||
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
|
tfdiags.Error,
|
||||||
|
"Failed to parse command-line flags",
|
||||||
|
err.Error(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
args = cmdFlags.Args()
|
||||||
|
if len(args) > 0 {
|
||||||
|
apply.PlanPath = args[0]
|
||||||
|
args = args[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
|
tfdiags.Error,
|
||||||
|
"Too many command line arguments",
|
||||||
|
"Expected at most one positional argument.",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
diags = diags.Append(apply.Operation.Parse())
|
||||||
|
|
||||||
|
switch {
|
||||||
|
default:
|
||||||
|
apply.ViewType = ViewHuman
|
||||||
|
}
|
||||||
|
|
||||||
|
return apply, diags
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
package arguments
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/hashicorp/terraform/addrs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseApply_basicValid(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
args []string
|
||||||
|
want *Apply
|
||||||
|
}{
|
||||||
|
"defaults": {
|
||||||
|
nil,
|
||||||
|
&Apply{
|
||||||
|
InputEnabled: true,
|
||||||
|
PlanPath: "",
|
||||||
|
ViewType: ViewHuman,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"disabled input and plan path": {
|
||||||
|
[]string{"-input=false", "saved.tfplan"},
|
||||||
|
&Apply{
|
||||||
|
InputEnabled: false,
|
||||||
|
PlanPath: "saved.tfplan",
|
||||||
|
ViewType: ViewHuman,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseApply_invalid(t *testing.T) {
|
||||||
|
got, diags := ParseApply([]string{"-frob"})
|
||||||
|
if len(diags) == 0 {
|
||||||
|
t.Fatal("expected diags but got none")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "flag provided but not defined"; !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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseApply_tooManyArguments(t *testing.T) {
|
||||||
|
got, diags := ParseApply([]string{"saved.tfplan", "please"})
|
||||||
|
if len(diags) == 0 {
|
||||||
|
t.Fatal("expected diags but got none")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Too many command line arguments"; !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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseApply_targets(t *testing.T) {
|
||||||
|
foobarbaz, _ := addrs.ParseTargetStr("foo_bar.baz")
|
||||||
|
boop, _ := addrs.ParseTargetStr("module.boop")
|
||||||
|
testCases := map[string]struct {
|
||||||
|
args []string
|
||||||
|
want []addrs.Targetable
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
"no targets by default": {
|
||||||
|
args: nil,
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
"one target": {
|
||||||
|
args: []string{"-target=foo_bar.baz"},
|
||||||
|
want: []addrs.Targetable{foobarbaz.Subject},
|
||||||
|
},
|
||||||
|
"two targets": {
|
||||||
|
args: []string{"-target=foo_bar.baz", "-target", "module.boop"},
|
||||||
|
want: []addrs.Targetable{foobarbaz.Subject, boop.Subject},
|
||||||
|
},
|
||||||
|
"invalid traversal": {
|
||||||
|
args: []string{"-target=foo."},
|
||||||
|
want: nil,
|
||||||
|
wantErr: "Dot must be followed by attribute name",
|
||||||
|
},
|
||||||
|
"invalid target": {
|
||||||
|
args: []string{"-target=data[0].foo"},
|
||||||
|
want: nil,
|
||||||
|
wantErr: "A data source name is required",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got, diags := ParseApply(tc.args)
|
||||||
|
if len(diags) > 0 {
|
||||||
|
if tc.wantErr == "" {
|
||||||
|
t.Fatalf("unexpected diags: %v", diags)
|
||||||
|
} else if got := diags.Err().Error(); !strings.Contains(got, tc.wantErr) {
|
||||||
|
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, tc.wantErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !cmp.Equal(got.Operation.Targets, tc.want) {
|
||||||
|
t.Fatalf("unexpected result\n%s", cmp.Diff(got.Operation.Targets, tc.want))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseApply_vars(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
args []string
|
||||||
|
want []FlagNameValue
|
||||||
|
}{
|
||||||
|
"no var flags by default": {
|
||||||
|
args: nil,
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
"one var": {
|
||||||
|
args: []string{"-var", "foo=bar"},
|
||||||
|
want: []FlagNameValue{
|
||||||
|
{Name: "-var", Value: "foo=bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"one var-file": {
|
||||||
|
args: []string{"-var-file", "cool.tfvars"},
|
||||||
|
want: []FlagNameValue{
|
||||||
|
{Name: "-var-file", Value: "cool.tfvars"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ordering preserved": {
|
||||||
|
args: []string{
|
||||||
|
"-var", "foo=bar",
|
||||||
|
"-var-file", "cool.tfvars",
|
||||||
|
"-var", "boop=beep",
|
||||||
|
},
|
||||||
|
want: []FlagNameValue{
|
||||||
|
{Name: "-var", Value: "foo=bar"},
|
||||||
|
{Name: "-var-file", Value: "cool.tfvars"},
|
||||||
|
{Name: "-var", Value: "boop=beep"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if vars := got.Vars.All(); !cmp.Equal(vars, tc.want) {
|
||||||
|
t.Fatalf("unexpected result\n%s", cmp.Diff(vars, tc.want))
|
||||||
|
}
|
||||||
|
if got, want := got.Vars.Empty(), len(tc.want) == 0; got != want {
|
||||||
|
t.Fatalf("expected Empty() to return %t, but was %t", want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
package arguments
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||||
|
"github.com/hashicorp/terraform/addrs"
|
||||||
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultParallelism is the limit Terraform places on total parallel
|
||||||
|
// operations as it walks the dependency graph.
|
||||||
|
const DefaultParallelism = 10
|
||||||
|
|
||||||
|
// State describes arguments which are used to define how Terraform interacts
|
||||||
|
// with state.
|
||||||
|
type State struct {
|
||||||
|
// Lock controls whether or not the state manager is used to lock state
|
||||||
|
// during operations.
|
||||||
|
Lock bool
|
||||||
|
|
||||||
|
// LockTimeout allows setting a time limit on acquiring the state lock.
|
||||||
|
// The default is 0, meaning no limit.
|
||||||
|
LockTimeout time.Duration
|
||||||
|
|
||||||
|
// StatePath specifies a non-default location for the state file. The
|
||||||
|
// default value is blank, which is interpeted as "terraform.tfstate".
|
||||||
|
StatePath string
|
||||||
|
|
||||||
|
// StateOutPath specifies a different path to write the final state file.
|
||||||
|
// The default value is blank, which results in state being written back to
|
||||||
|
// StatePath.
|
||||||
|
StateOutPath string
|
||||||
|
|
||||||
|
// BackupPath specifies the path where a backup copy of the state file will
|
||||||
|
// be stored before the new state is written. The default value is blank,
|
||||||
|
// which is interpreted as StateOutPath +
|
||||||
|
// ".backup".
|
||||||
|
BackupPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operation describes arguments which are used to configure how a Terraform
|
||||||
|
// operation such as a plan or apply executes.
|
||||||
|
type Operation struct {
|
||||||
|
// AutoApprove skips the manual verification step for the apply operation.
|
||||||
|
AutoApprove bool
|
||||||
|
|
||||||
|
// Parallelism is the limit Terraform places on total parallel operations
|
||||||
|
// as it walks the dependency graph.
|
||||||
|
Parallelism int
|
||||||
|
|
||||||
|
// Refresh controls whether or not the operation should refresh existing
|
||||||
|
// state before proceeding. Default is true.
|
||||||
|
Refresh bool
|
||||||
|
|
||||||
|
// Targets allow limiting an operation to a set of resource addresses and
|
||||||
|
// their dependencies.
|
||||||
|
Targets []addrs.Targetable
|
||||||
|
|
||||||
|
targetsRaw []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse must be called on Operation after initial flag parse. This processes
|
||||||
|
// the raw target flags into addrs.Targetable values, returning diagnostics if
|
||||||
|
// invalid.
|
||||||
|
func (o *Operation) Parse() tfdiags.Diagnostics {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
|
o.Targets = nil
|
||||||
|
|
||||||
|
for _, tr := range o.targetsRaw {
|
||||||
|
traversal, syntaxDiags := hclsyntax.ParseTraversalAbs([]byte(tr), "", hcl.Pos{Line: 1, Column: 1})
|
||||||
|
if syntaxDiags.HasErrors() {
|
||||||
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
|
tfdiags.Error,
|
||||||
|
fmt.Sprintf("Invalid target %q", tr),
|
||||||
|
syntaxDiags[0].Detail,
|
||||||
|
))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
target, targetDiags := addrs.ParseTarget(traversal)
|
||||||
|
if targetDiags.HasErrors() {
|
||||||
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
|
tfdiags.Error,
|
||||||
|
fmt.Sprintf("Invalid target %q", tr),
|
||||||
|
targetDiags[0].Description().Detail,
|
||||||
|
))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Targets = append(o.Targets, target.Subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vars describes arguments which specify non-default variable values. This
|
||||||
|
// interfce is unfortunately obscure, because the order of the CLI arguments
|
||||||
|
// determines the final value of the gathered variables. In future it might be
|
||||||
|
// desirable for the arguments package to handle the gathering of variables
|
||||||
|
// directly, returning a map of variable values.
|
||||||
|
type Vars struct {
|
||||||
|
vars *flagNameValueSlice
|
||||||
|
varFiles *flagNameValueSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vars) All() []FlagNameValue {
|
||||||
|
if v.vars == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v.vars.AllItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vars) Empty() bool {
|
||||||
|
if v.vars == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return v.vars.Empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
// extendedFlagSet creates a FlagSet with common backend, operation, and vars
|
||||||
|
// flags used in many commands. Target structs for each subset of flags must be
|
||||||
|
// provided in order to support those flags.
|
||||||
|
func extendedFlagSet(name string, state *State, operation *Operation, vars *Vars) *flag.FlagSet {
|
||||||
|
f := defaultFlagSet(name)
|
||||||
|
|
||||||
|
if state == nil && operation == nil && vars == nil {
|
||||||
|
panic("use defaultFlagSet")
|
||||||
|
}
|
||||||
|
|
||||||
|
if state != nil {
|
||||||
|
f.BoolVar(&state.Lock, "lock", true, "lock")
|
||||||
|
f.DurationVar(&state.LockTimeout, "lock-timeout", 0, "lock-timeout")
|
||||||
|
f.StringVar(&state.StatePath, "state", "", "state-path")
|
||||||
|
f.StringVar(&state.StateOutPath, "state-out", "", "state-path")
|
||||||
|
f.StringVar(&state.BackupPath, "backup", "", "backup-path")
|
||||||
|
}
|
||||||
|
|
||||||
|
if operation != nil {
|
||||||
|
f.BoolVar(&operation.AutoApprove, "auto-approve", false, "auto-approve")
|
||||||
|
f.IntVar(&operation.Parallelism, "parallelism", DefaultParallelism, "parallelism")
|
||||||
|
f.BoolVar(&operation.Refresh, "refresh", true, "refresh")
|
||||||
|
f.Var((*flagStringSlice)(&operation.targetsRaw), "target", "target")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather all -var and -var-file arguments into one heterogenous structure
|
||||||
|
// to preserve the overall order.
|
||||||
|
if vars != nil {
|
||||||
|
varsFlags := newFlagNameValueSlice("-var")
|
||||||
|
varFilesFlags := varsFlags.Alias("-var-file")
|
||||||
|
vars.vars = &varsFlags
|
||||||
|
vars.varFiles = &varFilesFlags
|
||||||
|
f.Var(vars.vars, "var", "var")
|
||||||
|
f.Var(vars.varFiles, "var-file", "var-file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return f
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package arguments
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// flagStringSlice is a flag.Value implementation which allows collecting
|
||||||
|
// multiple instances of a single flag into a slice. This is used for flags
|
||||||
|
// such as -target=aws_instance.foo and -var x=y.
|
||||||
|
type flagStringSlice []string
|
||||||
|
|
||||||
|
var _ flag.Value = (*flagStringSlice)(nil)
|
||||||
|
|
||||||
|
func (v *flagStringSlice) String() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
func (v *flagStringSlice) Set(raw string) error {
|
||||||
|
*v = append(*v, raw)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// flagNameValueSlice is a flag.Value implementation that appends raw flag
|
||||||
|
// names and values to a slice. This is used to collect a sequence of flags
|
||||||
|
// with possibly different names, preserving the overall order.
|
||||||
|
//
|
||||||
|
// FIXME: this is a copy of rawFlags from command/meta_config.go, with the
|
||||||
|
// eventual aim of replacing it altogether by gathering variables in the
|
||||||
|
// arguments package.
|
||||||
|
type flagNameValueSlice struct {
|
||||||
|
flagName string
|
||||||
|
items *[]FlagNameValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ flag.Value = flagNameValueSlice{}
|
||||||
|
|
||||||
|
func newFlagNameValueSlice(flagName string) flagNameValueSlice {
|
||||||
|
var items []FlagNameValue
|
||||||
|
return flagNameValueSlice{
|
||||||
|
flagName: flagName,
|
||||||
|
items: &items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f flagNameValueSlice) Empty() bool {
|
||||||
|
if f.items == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return len(*f.items) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f flagNameValueSlice) AllItems() []FlagNameValue {
|
||||||
|
if f.items == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return *f.items
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f flagNameValueSlice) Alias(flagName string) flagNameValueSlice {
|
||||||
|
return flagNameValueSlice{
|
||||||
|
flagName: flagName,
|
||||||
|
items: f.items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f flagNameValueSlice) String() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f flagNameValueSlice) Set(str string) error {
|
||||||
|
*f.items = append(*f.items, FlagNameValue{
|
||||||
|
Name: f.flagName,
|
||||||
|
Value: str,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlagNameValue struct {
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FlagNameValue) String() string {
|
||||||
|
return fmt.Sprintf("%s=%q", f.Name, f.Value)
|
||||||
|
}
|
|
@ -376,6 +376,9 @@ func (m *Meta) InterruptibleContext() (context.Context, context.CancelFunc) {
|
||||||
// operation itself is unsuccessful. Use the "Result" field of the
|
// operation itself is unsuccessful. Use the "Result" field of the
|
||||||
// returned operation object to recognize operation-level failure.
|
// returned operation object to recognize operation-level failure.
|
||||||
func (m *Meta) RunOperation(b backend.Enhanced, opReq *backend.Operation) (*backend.RunningOperation, error) {
|
func (m *Meta) RunOperation(b backend.Enhanced, opReq *backend.Operation) (*backend.RunningOperation, error) {
|
||||||
|
if opReq.View == nil {
|
||||||
|
panic("RunOperation called with nil View")
|
||||||
|
}
|
||||||
if opReq.ConfigDir != "" {
|
if opReq.ConfigDir != "" {
|
||||||
opReq.ConfigDir = m.normalizePath(opReq.ConfigDir)
|
opReq.ConfigDir = m.normalizePath(opReq.ConfigDir)
|
||||||
}
|
}
|
||||||
|
@ -392,14 +395,12 @@ func (m *Meta) RunOperation(b backend.Enhanced, opReq *backend.Operation) (*back
|
||||||
op.Stop()
|
op.Stop()
|
||||||
|
|
||||||
// Notify the user
|
// Notify the user
|
||||||
m.Ui.Output(outputInterrupt)
|
opReq.View.Interrupted()
|
||||||
|
|
||||||
// Still get the result, since there is still one
|
// Still get the result, since there is still one
|
||||||
select {
|
select {
|
||||||
case <-m.ShutdownCh:
|
case <-m.ShutdownCh:
|
||||||
m.Ui.Error(
|
opReq.View.FatalInterrupt()
|
||||||
"Two interrupts received. Exiting immediately. Note that data\n" +
|
|
||||||
"loss may have occurred.")
|
|
||||||
|
|
||||||
// cancel the operation completely
|
// cancel the operation completely
|
||||||
op.Cancel()
|
op.Cancel()
|
||||||
|
@ -782,3 +783,14 @@ func isAutoVarFile(path string) bool {
|
||||||
return strings.HasSuffix(path, ".auto.tfvars") ||
|
return strings.HasSuffix(path, ".auto.tfvars") ||
|
||||||
strings.HasSuffix(path, ".auto.tfvars.json")
|
strings.HasSuffix(path, ".auto.tfvars.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: as an interim refactoring step, we apply the contents of the state
|
||||||
|
// arguments directly to the Meta object. Future work would ideally update the
|
||||||
|
// code paths which use these arguments to be passed them directly for clarity.
|
||||||
|
func (m *Meta) applyStateArguments(args *arguments.State) {
|
||||||
|
m.stateLock = args.Lock
|
||||||
|
m.stateLockTimeout = args.LockTimeout
|
||||||
|
m.statePath = args.StatePath
|
||||||
|
m.stateOutPath = args.StateOutPath
|
||||||
|
m.backupPath = args.BackupPath
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/command/arguments"
|
||||||
|
"github.com/hashicorp/terraform/command/format"
|
||||||
|
"github.com/hashicorp/terraform/states"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The Apply view is used for the apply command.
|
||||||
|
type Apply interface {
|
||||||
|
ResourceCount(stateOutPath string)
|
||||||
|
Outputs(outputValues map[string]*states.OutputValue)
|
||||||
|
|
||||||
|
Operation() Operation
|
||||||
|
Hooks() []terraform.Hook
|
||||||
|
|
||||||
|
Diagnostics(diags tfdiags.Diagnostics)
|
||||||
|
HelpPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApply returns an initialized Apply implementation for the given ViewType.
|
||||||
|
func NewApply(vt arguments.ViewType, destroy bool, runningInAutomation bool, view *View) Apply {
|
||||||
|
switch vt {
|
||||||
|
case arguments.ViewHuman:
|
||||||
|
return &ApplyHuman{
|
||||||
|
View: *view,
|
||||||
|
destroy: destroy,
|
||||||
|
inAutomation: runningInAutomation,
|
||||||
|
countHook: &countHook{},
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unknown view type %v", vt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ApplyHuman implementation renders human-readable text logs, suitable for
|
||||||
|
// a scrolling terminal.
|
||||||
|
type ApplyHuman struct {
|
||||||
|
View
|
||||||
|
|
||||||
|
destroy bool
|
||||||
|
inAutomation bool
|
||||||
|
|
||||||
|
countHook *countHook
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Apply = (*ApplyHuman)(nil)
|
||||||
|
|
||||||
|
func (v *ApplyHuman) ResourceCount(stateOutPath string) {
|
||||||
|
if v.destroy {
|
||||||
|
v.streams.Printf(
|
||||||
|
v.colorize.Color("[reset][bold][green]\nDestroy complete! Resources: %d destroyed.\n"),
|
||||||
|
v.countHook.Removed,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
v.streams.Printf(
|
||||||
|
v.colorize.Color("[reset][bold][green]\nApply complete! Resources: %d added, %d changed, %d destroyed.\n"),
|
||||||
|
v.countHook.Added,
|
||||||
|
v.countHook.Changed,
|
||||||
|
v.countHook.Removed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (v.countHook.Added > 0 || v.countHook.Changed > 0) && stateOutPath != "" {
|
||||||
|
v.streams.Printf("\n%s\n\n", format.WordWrap(stateOutPathPostApply, v.View.outputColumns()))
|
||||||
|
v.streams.Printf("State path: %s\n", stateOutPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ApplyHuman) Outputs(outputValues map[string]*states.OutputValue) {
|
||||||
|
if len(outputValues) > 0 {
|
||||||
|
v.streams.Print(v.colorize.Color("[reset][bold][green]\nOutputs:\n\n"))
|
||||||
|
NewOutput(arguments.ViewHuman, &v.View).Output("", outputValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ApplyHuman) Operation() Operation {
|
||||||
|
return NewOperation(arguments.ViewHuman, v.inAutomation, &v.View)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ApplyHuman) Hooks() []terraform.Hook {
|
||||||
|
return []terraform.Hook{
|
||||||
|
v.countHook,
|
||||||
|
NewUiHook(&v.View),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ApplyHuman) HelpPrompt() {
|
||||||
|
command := "apply"
|
||||||
|
if v.destroy {
|
||||||
|
command = "destroy"
|
||||||
|
}
|
||||||
|
v.View.HelpPrompt(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateOutPathPostApply = "The state of your infrastructure has been saved to the path below. This state is required to modify and destroy your infrastructure, so keep it safe. To inspect the complete state use the `terraform show` command."
|
|
@ -0,0 +1,214 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/command/arguments"
|
||||||
|
"github.com/hashicorp/terraform/internal/terminal"
|
||||||
|
"github.com/hashicorp/terraform/states"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This test is mostly because I am paranoid about having two consecutive
|
||||||
|
// boolean arguments.
|
||||||
|
func TestApply_new(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
defer done(t)
|
||||||
|
v := NewApply(arguments.ViewHuman, false, true, NewView(streams))
|
||||||
|
hv, ok := v.(*ApplyHuman)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected return type %t", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hv.destroy != false {
|
||||||
|
t.Fatalf("unexpected destroy value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hv.inAutomation != true {
|
||||||
|
t.Fatalf("unexpected inAutomation value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic test coverage of Outputs, since most of its functionality is tested
|
||||||
|
// elsewhere.
|
||||||
|
func TestApplyHuman_outputs(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
v := NewApply(arguments.ViewHuman, false, false, NewView(streams))
|
||||||
|
|
||||||
|
v.Outputs(map[string]*states.OutputValue{
|
||||||
|
"foo": {Value: cty.StringVal("secret")},
|
||||||
|
})
|
||||||
|
|
||||||
|
got := done(t).Stdout()
|
||||||
|
for _, want := range []string{"Outputs:", `foo = "secret"`} {
|
||||||
|
if !strings.Contains(got, want) {
|
||||||
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outputs should do nothing if there are no outputs to render.
|
||||||
|
func TestApplyHuman_outputsEmpty(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
v := NewApply(arguments.ViewHuman, false, false, NewView(streams))
|
||||||
|
|
||||||
|
v.Outputs(map[string]*states.OutputValue{})
|
||||||
|
|
||||||
|
got := done(t).Stdout()
|
||||||
|
if got != "" {
|
||||||
|
t.Errorf("output should be empty, but got: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the correct view type and in-automation settings propagate to the
|
||||||
|
// Operation view.
|
||||||
|
func TestApplyHuman_operation(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
defer done(t)
|
||||||
|
v := NewApply(arguments.ViewHuman, false, true, NewView(streams)).Operation()
|
||||||
|
if hv, ok := v.(*OperationHuman); !ok {
|
||||||
|
t.Fatalf("unexpected return type %t", v)
|
||||||
|
} else if hv.inAutomation != true {
|
||||||
|
t.Fatalf("unexpected inAutomation value on Operation view")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This view is used for both apply and destroy commands, so the help output
|
||||||
|
// needs to cover both.
|
||||||
|
func TestApplyHuman_help(t *testing.T) {
|
||||||
|
testCases := map[string]bool{
|
||||||
|
"apply": false,
|
||||||
|
"destroy": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, destroy := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
v := NewApply(arguments.ViewHuman, destroy, false, NewView(streams))
|
||||||
|
v.HelpPrompt()
|
||||||
|
got := done(t).Stderr()
|
||||||
|
if !strings.Contains(got, name) {
|
||||||
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hooks and ResourceCount are tangled up and easiest to test together.
|
||||||
|
func TestApplyHuman_resourceCount(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
destroy bool
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
"apply": {
|
||||||
|
false,
|
||||||
|
"Apply complete! Resources: 1 added, 2 changed, 3 destroyed.",
|
||||||
|
},
|
||||||
|
"destroy": {
|
||||||
|
true,
|
||||||
|
"Destroy complete! Resources: 3 destroyed.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
v := NewApply(arguments.ViewHuman, tc.destroy, false, NewView(streams))
|
||||||
|
hooks := v.Hooks()
|
||||||
|
|
||||||
|
var count *countHook
|
||||||
|
for _, hook := range hooks {
|
||||||
|
if ch, ok := hook.(*countHook); ok {
|
||||||
|
count = ch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count == nil {
|
||||||
|
t.Fatalf("expected Hooks to include a countHook: %#v", hooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
count.Added = 1
|
||||||
|
count.Changed = 2
|
||||||
|
count.Removed = 3
|
||||||
|
|
||||||
|
v.ResourceCount("")
|
||||||
|
|
||||||
|
got := done(t).Stdout()
|
||||||
|
if !strings.Contains(got, tc.want) {
|
||||||
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyHuman_resourceCountStatePath(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
added int
|
||||||
|
changed int
|
||||||
|
removed int
|
||||||
|
statePath string
|
||||||
|
wantContains bool
|
||||||
|
}{
|
||||||
|
"default state path": {
|
||||||
|
added: 1,
|
||||||
|
changed: 2,
|
||||||
|
removed: 3,
|
||||||
|
statePath: "",
|
||||||
|
wantContains: false,
|
||||||
|
},
|
||||||
|
"only removed": {
|
||||||
|
added: 0,
|
||||||
|
changed: 0,
|
||||||
|
removed: 5,
|
||||||
|
statePath: "foo.tfstate",
|
||||||
|
wantContains: false,
|
||||||
|
},
|
||||||
|
"added": {
|
||||||
|
added: 5,
|
||||||
|
changed: 0,
|
||||||
|
removed: 0,
|
||||||
|
statePath: "foo.tfstate",
|
||||||
|
wantContains: true,
|
||||||
|
},
|
||||||
|
"changed": {
|
||||||
|
added: 5,
|
||||||
|
changed: 0,
|
||||||
|
removed: 0,
|
||||||
|
statePath: "foo.tfstate",
|
||||||
|
wantContains: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
v := NewApply(arguments.ViewHuman, false, false, NewView(streams))
|
||||||
|
hooks := v.Hooks()
|
||||||
|
|
||||||
|
var count *countHook
|
||||||
|
for _, hook := range hooks {
|
||||||
|
if ch, ok := hook.(*countHook); ok {
|
||||||
|
count = ch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count == nil {
|
||||||
|
t.Fatalf("expected Hooks to include a countHook: %#v", hooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
count.Added = tc.added
|
||||||
|
count.Changed = tc.changed
|
||||||
|
count.Removed = tc.removed
|
||||||
|
|
||||||
|
v.ResourceCount(tc.statePath)
|
||||||
|
|
||||||
|
got := done(t).Stdout()
|
||||||
|
want := "State path: " + tc.statePath
|
||||||
|
contains := strings.Contains(got, want)
|
||||||
|
if contains && !tc.wantContains {
|
||||||
|
t.Errorf("wrong result\ngot: %q\nshould not contain: %q", got, want)
|
||||||
|
} else if !contains && tc.wantContains {
|
||||||
|
t.Errorf("wrong result\ngot: %q\nshould contain: %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package command
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -11,9 +11,9 @@ import (
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CountHook is a hook that counts the number of resources
|
// countHook is a hook that counts the number of resources
|
||||||
// added, removed, changed during the course of an apply.
|
// added, removed, changed during the course of an apply.
|
||||||
type CountHook struct {
|
type countHook struct {
|
||||||
Added int
|
Added int
|
||||||
Changed int
|
Changed int
|
||||||
Removed int
|
Removed int
|
||||||
|
@ -29,9 +29,9 @@ type CountHook struct {
|
||||||
terraform.NilHook
|
terraform.NilHook
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ terraform.Hook = (*CountHook)(nil)
|
var _ terraform.Hook = (*countHook)(nil)
|
||||||
|
|
||||||
func (h *CountHook) Reset() {
|
func (h *countHook) Reset() {
|
||||||
h.Lock()
|
h.Lock()
|
||||||
defer h.Unlock()
|
defer h.Unlock()
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ func (h *CountHook) Reset() {
|
||||||
h.Removed = 0
|
h.Removed = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *CountHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) {
|
func (h *countHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) {
|
||||||
h.Lock()
|
h.Lock()
|
||||||
defer h.Unlock()
|
defer h.Unlock()
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ func (h *CountHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generati
|
||||||
return terraform.HookActionContinue, nil
|
return terraform.HookActionContinue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *CountHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (terraform.HookAction, error) {
|
func (h *countHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (terraform.HookAction, error) {
|
||||||
h.Lock()
|
h.Lock()
|
||||||
defer h.Unlock()
|
defer h.Unlock()
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ func (h *CountHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generat
|
||||||
return terraform.HookActionContinue, nil
|
return terraform.HookActionContinue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *CountHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) {
|
func (h *countHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) {
|
||||||
h.Lock()
|
h.Lock()
|
||||||
defer h.Unlock()
|
defer h.Unlock()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package command
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
@ -15,11 +15,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCountHook_impl(t *testing.T) {
|
func TestCountHook_impl(t *testing.T) {
|
||||||
var _ terraform.Hook = new(CountHook)
|
var _ terraform.Hook = new(countHook)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCountHookPostDiff_DestroyDeposed(t *testing.T) {
|
func TestCountHookPostDiff_DestroyDeposed(t *testing.T) {
|
||||||
h := new(CountHook)
|
h := new(countHook)
|
||||||
|
|
||||||
resources := map[string]*legacy.InstanceDiff{
|
resources := map[string]*legacy.InstanceDiff{
|
||||||
"lorem": &legacy.InstanceDiff{DestroyDeposed: true},
|
"lorem": &legacy.InstanceDiff{DestroyDeposed: true},
|
||||||
|
@ -35,7 +35,7 @@ func TestCountHookPostDiff_DestroyDeposed(t *testing.T) {
|
||||||
h.PostDiff(addr, states.DeposedKey("deadbeef"), plans.Delete, cty.DynamicVal, cty.DynamicVal)
|
h.PostDiff(addr, states.DeposedKey("deadbeef"), plans.Delete, cty.DynamicVal, cty.DynamicVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := new(CountHook)
|
expected := new(countHook)
|
||||||
expected.ToAdd = 0
|
expected.ToAdd = 0
|
||||||
expected.ToChange = 0
|
expected.ToChange = 0
|
||||||
expected.ToRemoveAndAdd = 0
|
expected.ToRemoveAndAdd = 0
|
||||||
|
@ -47,7 +47,7 @@ func TestCountHookPostDiff_DestroyDeposed(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCountHookPostDiff_DestroyOnly(t *testing.T) {
|
func TestCountHookPostDiff_DestroyOnly(t *testing.T) {
|
||||||
h := new(CountHook)
|
h := new(countHook)
|
||||||
|
|
||||||
resources := map[string]*legacy.InstanceDiff{
|
resources := map[string]*legacy.InstanceDiff{
|
||||||
"foo": &legacy.InstanceDiff{Destroy: true},
|
"foo": &legacy.InstanceDiff{Destroy: true},
|
||||||
|
@ -66,7 +66,7 @@ func TestCountHookPostDiff_DestroyOnly(t *testing.T) {
|
||||||
h.PostDiff(addr, states.CurrentGen, plans.Delete, cty.DynamicVal, cty.DynamicVal)
|
h.PostDiff(addr, states.CurrentGen, plans.Delete, cty.DynamicVal, cty.DynamicVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := new(CountHook)
|
expected := new(countHook)
|
||||||
expected.ToAdd = 0
|
expected.ToAdd = 0
|
||||||
expected.ToChange = 0
|
expected.ToChange = 0
|
||||||
expected.ToRemoveAndAdd = 0
|
expected.ToRemoveAndAdd = 0
|
||||||
|
@ -78,7 +78,7 @@ func TestCountHookPostDiff_DestroyOnly(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCountHookPostDiff_AddOnly(t *testing.T) {
|
func TestCountHookPostDiff_AddOnly(t *testing.T) {
|
||||||
h := new(CountHook)
|
h := new(countHook)
|
||||||
|
|
||||||
resources := map[string]*legacy.InstanceDiff{
|
resources := map[string]*legacy.InstanceDiff{
|
||||||
"foo": &legacy.InstanceDiff{
|
"foo": &legacy.InstanceDiff{
|
||||||
|
@ -108,7 +108,7 @@ func TestCountHookPostDiff_AddOnly(t *testing.T) {
|
||||||
h.PostDiff(addr, states.CurrentGen, plans.Create, cty.DynamicVal, cty.DynamicVal)
|
h.PostDiff(addr, states.CurrentGen, plans.Create, cty.DynamicVal, cty.DynamicVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := new(CountHook)
|
expected := new(countHook)
|
||||||
expected.ToAdd = 3
|
expected.ToAdd = 3
|
||||||
expected.ToChange = 0
|
expected.ToChange = 0
|
||||||
expected.ToRemoveAndAdd = 0
|
expected.ToRemoveAndAdd = 0
|
||||||
|
@ -120,7 +120,7 @@ func TestCountHookPostDiff_AddOnly(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCountHookPostDiff_ChangeOnly(t *testing.T) {
|
func TestCountHookPostDiff_ChangeOnly(t *testing.T) {
|
||||||
h := new(CountHook)
|
h := new(countHook)
|
||||||
|
|
||||||
resources := map[string]*legacy.InstanceDiff{
|
resources := map[string]*legacy.InstanceDiff{
|
||||||
"foo": &legacy.InstanceDiff{
|
"foo": &legacy.InstanceDiff{
|
||||||
|
@ -153,7 +153,7 @@ func TestCountHookPostDiff_ChangeOnly(t *testing.T) {
|
||||||
h.PostDiff(addr, states.CurrentGen, plans.Update, cty.DynamicVal, cty.DynamicVal)
|
h.PostDiff(addr, states.CurrentGen, plans.Update, cty.DynamicVal, cty.DynamicVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := new(CountHook)
|
expected := new(countHook)
|
||||||
expected.ToAdd = 0
|
expected.ToAdd = 0
|
||||||
expected.ToChange = 3
|
expected.ToChange = 3
|
||||||
expected.ToRemoveAndAdd = 0
|
expected.ToRemoveAndAdd = 0
|
||||||
|
@ -165,7 +165,7 @@ func TestCountHookPostDiff_ChangeOnly(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCountHookPostDiff_Mixed(t *testing.T) {
|
func TestCountHookPostDiff_Mixed(t *testing.T) {
|
||||||
h := new(CountHook)
|
h := new(countHook)
|
||||||
|
|
||||||
resources := map[string]plans.Action{
|
resources := map[string]plans.Action{
|
||||||
"foo": plans.Delete,
|
"foo": plans.Delete,
|
||||||
|
@ -184,7 +184,7 @@ func TestCountHookPostDiff_Mixed(t *testing.T) {
|
||||||
h.PostDiff(addr, states.CurrentGen, a, cty.DynamicVal, cty.DynamicVal)
|
h.PostDiff(addr, states.CurrentGen, a, cty.DynamicVal, cty.DynamicVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := new(CountHook)
|
expected := new(countHook)
|
||||||
expected.ToAdd = 0
|
expected.ToAdd = 0
|
||||||
expected.ToChange = 1
|
expected.ToChange = 1
|
||||||
expected.ToRemoveAndAdd = 0
|
expected.ToRemoveAndAdd = 0
|
||||||
|
@ -197,7 +197,7 @@ func TestCountHookPostDiff_Mixed(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCountHookPostDiff_NoChange(t *testing.T) {
|
func TestCountHookPostDiff_NoChange(t *testing.T) {
|
||||||
h := new(CountHook)
|
h := new(countHook)
|
||||||
|
|
||||||
resources := map[string]*legacy.InstanceDiff{
|
resources := map[string]*legacy.InstanceDiff{
|
||||||
"foo": &legacy.InstanceDiff{},
|
"foo": &legacy.InstanceDiff{},
|
||||||
|
@ -216,7 +216,7 @@ func TestCountHookPostDiff_NoChange(t *testing.T) {
|
||||||
h.PostDiff(addr, states.CurrentGen, plans.NoOp, cty.DynamicVal, cty.DynamicVal)
|
h.PostDiff(addr, states.CurrentGen, plans.NoOp, cty.DynamicVal, cty.DynamicVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := new(CountHook)
|
expected := new(countHook)
|
||||||
expected.ToAdd = 0
|
expected.ToAdd = 0
|
||||||
expected.ToChange = 0
|
expected.ToChange = 0
|
||||||
expected.ToRemoveAndAdd = 0
|
expected.ToRemoveAndAdd = 0
|
||||||
|
@ -229,7 +229,7 @@ func TestCountHookPostDiff_NoChange(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCountHookPostDiff_DataSource(t *testing.T) {
|
func TestCountHookPostDiff_DataSource(t *testing.T) {
|
||||||
h := new(CountHook)
|
h := new(countHook)
|
||||||
|
|
||||||
resources := map[string]plans.Action{
|
resources := map[string]plans.Action{
|
||||||
"foo": plans.Delete,
|
"foo": plans.Delete,
|
||||||
|
@ -248,7 +248,7 @@ func TestCountHookPostDiff_DataSource(t *testing.T) {
|
||||||
h.PostDiff(addr, states.CurrentGen, a, cty.DynamicVal, cty.DynamicVal)
|
h.PostDiff(addr, states.CurrentGen, a, cty.DynamicVal, cty.DynamicVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := new(CountHook)
|
expected := new(countHook)
|
||||||
expected.ToAdd = 0
|
expected.ToAdd = 0
|
||||||
expected.ToChange = 0
|
expected.ToChange = 0
|
||||||
expected.ToRemoveAndAdd = 0
|
expected.ToRemoveAndAdd = 0
|
||||||
|
@ -261,7 +261,7 @@ func TestCountHookPostDiff_DataSource(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCountHookApply_ChangeOnly(t *testing.T) {
|
func TestCountHookApply_ChangeOnly(t *testing.T) {
|
||||||
h := new(CountHook)
|
h := new(countHook)
|
||||||
|
|
||||||
resources := map[string]*legacy.InstanceDiff{
|
resources := map[string]*legacy.InstanceDiff{
|
||||||
"foo": &legacy.InstanceDiff{
|
"foo": &legacy.InstanceDiff{
|
||||||
|
@ -295,7 +295,7 @@ func TestCountHookApply_ChangeOnly(t *testing.T) {
|
||||||
h.PostApply(addr, states.CurrentGen, cty.DynamicVal, nil)
|
h.PostApply(addr, states.CurrentGen, cty.DynamicVal, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := &CountHook{pending: make(map[string]plans.Action)}
|
expected := &countHook{pending: make(map[string]plans.Action)}
|
||||||
expected.Added = 0
|
expected.Added = 0
|
||||||
expected.Changed = 3
|
expected.Changed = 3
|
||||||
expected.Removed = 0
|
expected.Removed = 0
|
||||||
|
@ -306,7 +306,7 @@ func TestCountHookApply_ChangeOnly(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCountHookApply_DestroyOnly(t *testing.T) {
|
func TestCountHookApply_DestroyOnly(t *testing.T) {
|
||||||
h := new(CountHook)
|
h := new(countHook)
|
||||||
|
|
||||||
resources := map[string]*legacy.InstanceDiff{
|
resources := map[string]*legacy.InstanceDiff{
|
||||||
"foo": &legacy.InstanceDiff{Destroy: true},
|
"foo": &legacy.InstanceDiff{Destroy: true},
|
||||||
|
@ -326,7 +326,7 @@ func TestCountHookApply_DestroyOnly(t *testing.T) {
|
||||||
h.PostApply(addr, states.CurrentGen, cty.DynamicVal, nil)
|
h.PostApply(addr, states.CurrentGen, cty.DynamicVal, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := &CountHook{pending: make(map[string]plans.Action)}
|
expected := &countHook{pending: make(map[string]plans.Action)}
|
||||||
expected.Added = 0
|
expected.Added = 0
|
||||||
expected.Changed = 0
|
expected.Changed = 0
|
||||||
expected.Removed = 4
|
expected.Removed = 4
|
|
@ -15,6 +15,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Operation interface {
|
type Operation interface {
|
||||||
|
Interrupted()
|
||||||
|
FatalInterrupt()
|
||||||
Stopping()
|
Stopping()
|
||||||
Cancelled(destroy bool)
|
Cancelled(destroy bool)
|
||||||
|
|
||||||
|
@ -51,6 +53,24 @@ type OperationHuman struct {
|
||||||
|
|
||||||
var _ Operation = (*OperationHuman)(nil)
|
var _ Operation = (*OperationHuman)(nil)
|
||||||
|
|
||||||
|
func (v *OperationHuman) Interrupted() {
|
||||||
|
v.streams.Println(format.WordWrap(interrupted, v.outputColumns()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OperationHuman) FatalInterrupt() {
|
||||||
|
v.streams.Eprintln(format.WordWrap(fatalInterrupt, v.errorColumns()))
|
||||||
|
}
|
||||||
|
|
||||||
|
const fatalInterrupt = `
|
||||||
|
Two interrupts received. Exiting immediately. Note that data loss may have occurred.
|
||||||
|
`
|
||||||
|
|
||||||
|
const interrupted = `
|
||||||
|
Interrupt received.
|
||||||
|
Please wait for Terraform to exit or data loss may occur.
|
||||||
|
Gracefully shutting down...
|
||||||
|
`
|
||||||
|
|
||||||
func (v *OperationHuman) Stopping() {
|
func (v *OperationHuman) Stopping() {
|
||||||
v.streams.Println("Stopping operation...")
|
v.streams.Println("Stopping operation...")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue