package local import ( "bytes" "context" "fmt" "log" "sort" "strings" "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/command/format" "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" ) func (b *Local) opPlan( stopCtx context.Context, cancelCtx context.Context, op *backend.Operation, runningOp *backend.RunningOperation) { log.Printf("[INFO] backend/local: starting Plan operation") var diags tfdiags.Diagnostics outputColumns := b.outputColumns() if op.PlanFile != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Can't re-plan a saved plan", "The plan command was given a saved plan file as its input. This command generates "+ "a new plan, and so it requires a configuration directory as its argument.", )) b.ReportResult(runningOp, diags) return } // Local planning requires a config, unless we're planning to destroy. if !op.Destroy && !op.HasConfig() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "No configuration files", "Plan requires configuration to be present. Planning without a configuration would "+ "mark everything for destruction, which is normally not what is desired. If you "+ "would like to destroy everything, run plan with the -destroy option. Otherwise, "+ "create a Terraform configuration file (.tf file) and try again.", )) b.ReportResult(runningOp, diags) return } if b.ContextOpts == nil { b.ContextOpts = new(terraform.ContextOpts) } // Get our context tfCtx, configSnap, opState, ctxDiags := b.context(op) diags = diags.Append(ctxDiags) if ctxDiags.HasErrors() { b.ReportResult(runningOp, diags) return } // the state was locked during succesfull context creation; unlock the state // when the operation completes defer func() { err := op.StateLocker.Unlock(nil) if err != nil { b.ShowDiagnostics(err) runningOp.Result = backend.OperationFailure } }() runningOp.State = tfCtx.State() // Perform the plan in a goroutine so we can be interrupted var plan *plans.Plan var planDiags tfdiags.Diagnostics doneCh := make(chan struct{}) go func() { defer close(doneCh) log.Printf("[INFO] backend/local: plan calling Plan") plan, planDiags = tfCtx.Plan() }() if b.opWait(doneCh, stopCtx, cancelCtx, tfCtx, opState) { // If we get in here then the operation was cancelled, which is always // considered to be a failure. log.Printf("[INFO] backend/local: plan operation was force-cancelled by interrupt") runningOp.Result = backend.OperationFailure return } log.Printf("[INFO] backend/local: plan operation completed") diags = diags.Append(planDiags) if planDiags.HasErrors() { b.ReportResult(runningOp, diags) return } // Record whether this plan includes any side-effects that could be applied. runningOp.PlanEmpty = plan.Changes.Empty() // Save the plan to disk if path := op.PlanOutPath; path != "" { if op.PlanOutBackend == nil { // This is always a bug in the operation caller; it's not valid // to set PlanOutPath without also setting PlanOutBackend. diags = diags.Append(fmt.Errorf( "PlanOutPath set without also setting PlanOutBackend (this is a bug in Terraform)"), ) b.ReportResult(runningOp, diags) return } plan.Backend = *op.PlanOutBackend // We may have updated the state in the refresh step above, but we // will freeze that updated state in the plan file for now and // only write it if this plan is subsequently applied. plannedStateFile := statemgr.PlannedStateUpdate(opState, plan.State) log.Printf("[INFO] backend/local: writing plan output to: %s", path) err := planfile.Create(path, configSnap, plannedStateFile, plan) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Failed to write plan file", fmt.Sprintf("The plan file could not be written: %s.", err), )) b.ReportResult(runningOp, diags) return } } // Perform some output tasks if we have a CLI to output to. if b.CLI != nil { schemas := tfCtx.Schemas() if runningOp.PlanEmpty { b.CLI.Output("\n" + b.Colorize().Color(strings.TrimSpace(planNoChanges))) b.CLI.Output("\n" + strings.TrimSpace(format.WordWrap(planNoChangesDetail, outputColumns))) // Even if there are no changes, there still could be some warnings b.ShowDiagnostics(diags) return } b.renderPlan(plan, plan.State, schemas) // If we've accumulated any warnings along the way then we'll show them // here just before we show the summary and next steps. If we encountered // errors then we would've returned early at some other point above. b.ShowDiagnostics(diags) // Give the user some next-steps, unless we're running in an automation // tool which is presumed to provide its own UI for further actions. if !b.RunningInAutomation { b.outputHorizRule() if path := op.PlanOutPath; path == "" { b.CLI.Output(fmt.Sprintf( "\n" + strings.TrimSpace(format.WordWrap(planHeaderNoOutput, outputColumns)) + "\n", )) } else { b.CLI.Output(fmt.Sprintf( "\n"+strings.TrimSpace(format.WordWrap(planHeaderYesOutput, outputColumns))+"\n", path, path, )) } } } } func (b *Local) renderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Schemas) { RenderPlan(plan, baseState, schemas, b.CLI, b.Colorize(), b.outputColumns()) } // RenderPlan renders the given plan to the given UI. // // This is exported only so that the "terraform show" command can re-use it. // Ideally it would be somewhere outside of this backend code so that both // can call into it, but we're leaving it here for now in order to avoid // disruptive refactoring. // // If you find yourself wanting to call this function from a third callsite, // please consider whether it's time to do the more disruptive refactoring // so that something other than the local backend package is offering this // functionality. // // The difference between baseState and priorState is that baseState is the // result of implicitly running refresh (unless that was disabled) while // priorState is a snapshot of the state as it was before we took any actions // at all. priorState can optionally be nil if the caller has only a saved // plan and not the prior state it was built from. In that case, changes to // output values will not currently be rendered because their prior values // are currently stored only in the prior state. (see the docstring for // func planHasSideEffects for why this is and when that might change) func RenderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Schemas, ui cli.Ui, colorize *colorstring.Colorize, width int) { counts := map[plans.Action]int{} var rChanges []*plans.ResourceInstanceChangeSrc for _, change := range plan.Changes.Resources { if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode { // Avoid rendering data sources on deletion continue } rChanges = append(rChanges, change) counts[change.Action]++ } headerBuf := &bytes.Buffer{} fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(format.WordWrap(planHeaderIntro, width))) if counts[plans.Create] > 0 { fmt.Fprintf(headerBuf, "%s create\n", format.DiffActionSymbol(plans.Create)) } if counts[plans.Update] > 0 { fmt.Fprintf(headerBuf, "%s update in-place\n", format.DiffActionSymbol(plans.Update)) } if counts[plans.Delete] > 0 { fmt.Fprintf(headerBuf, "%s destroy\n", format.DiffActionSymbol(plans.Delete)) } if counts[plans.DeleteThenCreate] > 0 { fmt.Fprintf(headerBuf, "%s destroy and then create replacement\n", format.DiffActionSymbol(plans.DeleteThenCreate)) } if counts[plans.CreateThenDelete] > 0 { fmt.Fprintf(headerBuf, "%s create replacement and then destroy\n", format.DiffActionSymbol(plans.CreateThenDelete)) } if counts[plans.Read] > 0 { fmt.Fprintf(headerBuf, "%s read (data resources)\n", format.DiffActionSymbol(plans.Read)) } ui.Output(colorize.Color(headerBuf.String())) ui.Output("Terraform will perform the following actions:\n") // Note: we're modifying the backing slice of this plan object in-place // here. The ordering of resource changes in a plan is not significant, // but we can only do this safely here because we can assume that nobody // is concurrently modifying our changes while we're trying to print it. sort.Slice(rChanges, func(i, j int) bool { iA := rChanges[i].Addr jA := rChanges[j].Addr if iA.String() == jA.String() { return rChanges[i].DeposedKey < rChanges[j].DeposedKey } return iA.Less(jA) }) for _, rcs := range rChanges { if rcs.Action == plans.NoOp { continue } providerSchema := schemas.ProviderSchema(rcs.ProviderAddr.Provider) if providerSchema == nil { // Should never happen ui.Output(fmt.Sprintf("(schema missing for %s)\n", rcs.ProviderAddr)) continue } rSchema, _ := providerSchema.SchemaForResourceAddr(rcs.Addr.Resource.Resource) if rSchema == nil { // Should never happen ui.Output(fmt.Sprintf("(schema missing for %s)\n", rcs.Addr)) continue } // check if the change is due to a tainted resource tainted := false if !baseState.Empty() { if is := baseState.ResourceInstance(rcs.Addr); is != nil { if obj := is.GetGeneration(rcs.DeposedKey.Generation()); obj != nil { tainted = obj.Status == states.ObjectTainted } } } ui.Output(format.ResourceChange( rcs, tainted, rSchema, colorize, )) } // stats is similar to counts above, but: // - it considers only resource changes // - it simplifies "replace" into both a create and a delete stats := map[plans.Action]int{} for _, change := range rChanges { switch change.Action { case plans.CreateThenDelete, plans.DeleteThenCreate: stats[plans.Create]++ stats[plans.Delete]++ default: stats[change.Action]++ } } ui.Output(colorize.Color(fmt.Sprintf( "[reset][bold]Plan:[reset] "+ "%d to add, %d to change, %d to destroy.", stats[plans.Create], stats[plans.Update], stats[plans.Delete], ))) // If there is at least one planned change to the root module outputs // then we'll render a summary of those too. var changedRootModuleOutputs []*plans.OutputChangeSrc for _, output := range plan.Changes.Outputs { if !output.Addr.Module.IsRoot() { continue } if output.ChangeSrc.Action == plans.NoOp { continue } changedRootModuleOutputs = append(changedRootModuleOutputs, output) } if len(changedRootModuleOutputs) > 0 { ui.Output(colorize.Color("[reset]\n[bold]Changes to Outputs:[reset]" + format.OutputChanges(changedRootModuleOutputs, colorize))) } } const planHeaderIntro = ` Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: ` const planHeaderNoOutput = ` Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now. ` const planHeaderYesOutput = ` Saved the plan to: %s To perform exactly these actions, run the following command to apply: terraform apply %q ` const planNoChanges = ` [reset][bold][green]No changes. Infrastructure is up-to-date.[reset][green] ` const planNoChangesDetail = ` That Terraform did not detect any differences between your configuration and the remote system(s). As a result, there are no actions to take. `