package command import ( "fmt" "strings" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/command/arguments" "github.com/hashicorp/terraform/command/views" "github.com/hashicorp/terraform/tfdiags" ) // PlanCommand is a Command implementation that compares a Terraform // configuration to an actual infrastructure and shows the differences. type PlanCommand struct { Meta } func (c *PlanCommand) Run(rawArgs []string) int { // Parse and apply global view arguments common, rawArgs := arguments.ParseView(rawArgs) c.View.Configure(common) // Parse and validate flags args, diags := arguments.ParsePlan(rawArgs) // Instantiate the view, even if there are flag errors, so that we render // diagnostics according to the desired view view := views.NewPlan(args.ViewType, c.RunningInAutomation, c.View) if diags.HasErrors() { view.Diagnostics(diags) view.HelpPrompt("plan") return 1 } // Check for user-supplied plugin path var err error if c.pluginPath, err = c.loadPluginPath(); err != nil { diags = diags.Append(err) 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 diags = diags.Append(c.providerDevOverrideRuntimeWarnings()) // Prepare the backend with the backend-specific arguments be, beDiags := c.PrepareBackend(args.State) diags = diags.Append(beDiags) if diags.HasErrors() { view.Diagnostics(diags) return 1 } // Build the operation request opReq, opDiags := c.OperationRequest(be, view, args.Operation, args.Destroy, args.OutPath) diags = diags.Append(opDiags) if diags.HasErrors() { view.Diagnostics(diags) return 1 } // Collect variable value and add them to the operation request diags = diags.Append(c.GatherVariables(opReq, args.Vars)) if diags.HasErrors() { view.Diagnostics(diags) return 1 } // 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) diags = nil // Perform 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() } if args.DetailedExitCode && !op.PlanEmpty { return 2 } return op.Result.ExitStatus() } func (c *PlanCommand) PrepareBackend(args *arguments.State) (backend.Enhanced, 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) backendConfig, diags := c.loadBackendConfig(".") if diags.HasErrors() { return nil, diags } // Load the backend be, beDiags := c.Backend(&BackendOpts{ Config: backendConfig, }) diags = diags.Append(beDiags) if beDiags.HasErrors() { return nil, diags } return be, diags } 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 // Build the operation opReq := c.Operation(be) opReq.ConfigDir = "." opReq.Destroy = destroy opReq.Hooks = view.Hooks() opReq.PlanRefresh = args.Refresh opReq.PlanOutPath = planOutPath opReq.Targets = args.Targets opReq.Type = backend.OperationTypePlan opReq.View = view.Operation() // FIXME: this shim is needed until the remote backend is migrated to views opReq.ShowDiagnostics = func(vals ...interface{}) { var diags tfdiags.Diagnostics diags = diags.Append(vals...) view.Diagnostics(diags) } var err error opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { diags = diags.Append(fmt.Errorf("Failed to initialize config loader: %s", err)) return nil, diags } return opReq, diags } func (c *PlanCommand) GatherVariables(opReq *backend.Operation, args *arguments.Vars) tfdiags.Diagnostics { var diags tfdiags.Diagnostics // FIXME the arguments package currently trivially gathers variable related // 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. varArgs := args.All() items := make([]rawFlag, len(varArgs)) for i := range varArgs { items[i].Name = varArgs[i].Name items[i].Value = varArgs[i].Value } c.Meta.variableArgs = rawFlags{items: &items} opReq.Variables, diags = c.collectVariableValues() return diags } func (c *PlanCommand) Help() string { helpText := ` Usage: terraform plan [options] Generates a speculative execution plan, showing what actions Terraform would take to apply the current configuration. This command will not actually perform the planned actions. 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: -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) 1 - Errored 2 - Succeeded, there is a diff -input=true Ask for input for variables if not directly set. -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. -out=path Write a plan file to the given path. This can be used as input to the "apply" command. -parallelism=n Limit the number of concurrent operations. Defaults to 10. -refresh=true Update state prior to checking for differences. -state=statefile Path to a Terraform state file to use to look up Terraform-managed resources. By default it will use the state "terraform.tfstate" if it exists. -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) } func (c *PlanCommand) Synopsis() string { return "Show changes required by the current configuration" }