package command import ( "fmt" "strings" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/plans" "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(args []string) int { var destroy, refresh, detailed bool var outPath string args = c.Meta.process(args) cmdFlags := c.Meta.extendedFlagSet("plan") cmdFlags.BoolVar(&destroy, "destroy", false, "destroy") cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") cmdFlags.StringVar(&outPath, "out", "", "path") cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism") cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") cmdFlags.BoolVar(&detailed, "detailed-exitcode", false, "detailed-exitcode") 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 { return 1 } configPath, err := ModulePath(cmdFlags.Args()) if err != nil { c.Ui.Error(err.Error()) return 1 } // Check for user-supplied plugin path if c.pluginPath, err = c.loadPluginPath(); err != nil { c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err)) return 1 } // Check if the path is a plan, which is not permitted planFileReader, err := c.PlanFile(configPath) if err != nil { c.Ui.Error(err.Error()) return 1 } if planFileReader != nil { c.showDiagnostics(tfdiags.Sourceless( tfdiags.Error, "Invalid configuration directory", fmt.Sprintf("Cannot pass a saved plan file to the 'terraform plan' command. To apply a saved plan, use: terraform apply %s", configPath), )) return 1 } var diags tfdiags.Diagnostics var backendConfig *configs.Backend var configDiags tfdiags.Diagnostics backendConfig, configDiags = c.loadBackendConfig(configPath) diags = diags.Append(configDiags) if configDiags.HasErrors() { c.showDiagnostics(diags) return 1 } // Load the backend b, backendDiags := c.Backend(&BackendOpts{ Config: backendConfig, }) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) return 1 } // Emit any diagnostics we've accumulated before we delegate to the // backend, since the backend will handle its own diagnostics internally. c.showDiagnostics(diags) diags = nil // Build the operation opReq := c.Operation(b) opReq.ConfigDir = configPath opReq.Destroy = destroy opReq.PlanOutPath = outPath opReq.PlanRefresh = refresh opReq.Type = backend.OperationTypePlan opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { c.showDiagnostics(err) return 1 } { var moreDiags tfdiags.Diagnostics opReq.Variables, moreDiags = c.collectVariableValues() diags = diags.Append(moreDiags) if moreDiags.HasErrors() { c.showDiagnostics(diags) return 1 } } // c.Backend above has a non-obvious side-effect of also populating // c.backendState, which is the state-shaped formulation of the effective // backend configuration after evaluation of the backend configuration. // We will in turn adapt that to a plans.Backend to include in a plan file // if opReq.PlanOutPath was set to a non-empty value above. // // FIXME: It's ugly to be doing this inline here, but it's also not really // clear where would be better to do it. In future we should find a better // home for this logic, and ideally also stop depending on the side-effect // of c.Backend setting c.backendState. { // This is not actually a state in the usual sense, but rather a // representation of part of the current working directory's // "configuration state". backendPseudoState := c.backendState if backendPseudoState == nil { // Should never happen if c.Backend is behaving properly. diags = diags.Append(fmt.Errorf("Backend initialization didn't produce resolved configuration (This is a bug in Terraform)")) c.showDiagnostics(diags) return 1 } var backendForPlan plans.Backend backendForPlan.Type = backendPseudoState.Type workspace, err := c.Workspace() if err != nil { c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) return 1 } backendForPlan.Workspace = workspace // Configuration is a little more awkward to handle here because it's // stored in state as raw JSON but we need it as a plans.DynamicValue // to save it in the state. To do that conversion we need to know the // configuration schema of the backend. configSchema := b.ConfigSchema() config, err := backendPseudoState.Config(configSchema) if err != nil { // This means that the stored settings don't conform to the current // schema, which could either be because we're reading something // created by an older version that is no longer compatible, or // because the user manually tampered with the stored config. diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid backend initialization", fmt.Sprintf("The backend configuration for this working directory is not valid: %s.\n\nIf you have recently upgraded Terraform, you may need to re-run \"terraform init\" to re-initialize this working directory.", err), )) c.showDiagnostics(diags) return 1 } configForPlan, err := plans.NewDynamicValue(config, configSchema.ImpliedType()) if err != nil { // This should never happen, since we've just decoded this value // using the same schema. diags = diags.Append(fmt.Errorf("Failed to encode backend configuration to store in plan: %s", err)) c.showDiagnostics(diags) return 1 } backendForPlan.Config = configForPlan } // Perform the operation op, err := c.RunOperation(b, opReq) if err != nil { c.showDiagnostics(err) return 1 } if op.Result != backend.OperationSuccess { return op.Result.ExitStatus() } if detailed && !op.PlanEmpty { return 2 } return op.Result.ExitStatus() } func (c *PlanCommand) Help() string { helpText := ` Usage: terraform plan [options] [DIR] Generates an execution plan for Terraform. This execution plan can be reviewed prior to running apply to get a sense for what Terraform will do. Optionally, the plan can be saved to a Terraform plan file, and apply can take this plan file to execute this plan exactly. 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 "Generate and show an execution plan" }