github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/backend/local/backend_plan.go (about) 1 package local 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 8 "github.com/hashicorp/terraform/internal/backend" 9 "github.com/hashicorp/terraform/internal/logging" 10 "github.com/hashicorp/terraform/internal/plans" 11 "github.com/hashicorp/terraform/internal/plans/planfile" 12 "github.com/hashicorp/terraform/internal/states/statefile" 13 "github.com/hashicorp/terraform/internal/states/statemgr" 14 "github.com/hashicorp/terraform/internal/terraform" 15 "github.com/hashicorp/terraform/internal/tfdiags" 16 ) 17 18 func (b *Local) opPlan( 19 stopCtx context.Context, 20 cancelCtx context.Context, 21 op *backend.Operation, 22 runningOp *backend.RunningOperation) { 23 24 log.Printf("[INFO] backend/local: starting Plan operation") 25 26 var diags tfdiags.Diagnostics 27 28 if op.PlanFile != nil { 29 diags = diags.Append(tfdiags.Sourceless( 30 tfdiags.Error, 31 "Can't re-plan a saved plan", 32 "The plan command was given a saved plan file as its input. This command generates "+ 33 "a new plan, and so it requires a configuration directory as its argument.", 34 )) 35 op.ReportResult(runningOp, diags) 36 return 37 } 38 39 // Local planning requires a config, unless we're planning to destroy. 40 if op.PlanMode != plans.DestroyMode && !op.HasConfig() { 41 diags = diags.Append(tfdiags.Sourceless( 42 tfdiags.Error, 43 "No configuration files", 44 "Plan requires configuration to be present. Planning without a configuration would "+ 45 "mark everything for destruction, which is normally not what is desired. If you "+ 46 "would like to destroy everything, run plan with the -destroy option. Otherwise, "+ 47 "create a Terraform configuration file (.tf file) and try again.", 48 )) 49 op.ReportResult(runningOp, diags) 50 return 51 } 52 53 if b.ContextOpts == nil { 54 b.ContextOpts = new(terraform.ContextOpts) 55 } 56 57 // Get our context 58 lr, configSnap, opState, ctxDiags := b.localRun(op) 59 diags = diags.Append(ctxDiags) 60 if ctxDiags.HasErrors() { 61 op.ReportResult(runningOp, diags) 62 return 63 } 64 // the state was locked during succesfull context creation; unlock the state 65 // when the operation completes 66 defer func() { 67 diags := op.StateLocker.Unlock() 68 if diags.HasErrors() { 69 op.View.Diagnostics(diags) 70 runningOp.Result = backend.OperationFailure 71 } 72 }() 73 74 // Since planning doesn't immediately change the persisted state, the 75 // resulting state is always just the input state. 76 runningOp.State = lr.InputState 77 78 // Perform the plan in a goroutine so we can be interrupted 79 var plan *plans.Plan 80 var planDiags tfdiags.Diagnostics 81 doneCh := make(chan struct{}) 82 go func() { 83 defer logging.PanicHandler() 84 defer close(doneCh) 85 log.Printf("[INFO] backend/local: plan calling Plan") 86 plan, planDiags = lr.Core.Plan(lr.Config, lr.InputState, lr.PlanOpts) 87 }() 88 89 if b.opWait(doneCh, stopCtx, cancelCtx, lr.Core, opState, op.View) { 90 // If we get in here then the operation was cancelled, which is always 91 // considered to be a failure. 92 log.Printf("[INFO] backend/local: plan operation was force-cancelled by interrupt") 93 runningOp.Result = backend.OperationFailure 94 return 95 } 96 log.Printf("[INFO] backend/local: plan operation completed") 97 98 // NOTE: We intentionally don't stop here on errors because we always want 99 // to try to present a partial plan report and, if the user chose to, 100 // generate a partial saved plan file for external analysis. 101 diags = diags.Append(planDiags) 102 103 // Even if there are errors we need to handle anything that may be 104 // contained within the plan, so only exit if there is no data at all. 105 if plan == nil { 106 runningOp.PlanEmpty = true 107 op.ReportResult(runningOp, diags) 108 return 109 } 110 111 // Record whether this plan includes any side-effects that could be applied. 112 runningOp.PlanEmpty = !plan.CanApply() 113 114 // Save the plan to disk 115 if path := op.PlanOutPath; path != "" { 116 if op.PlanOutBackend == nil { 117 // This is always a bug in the operation caller; it's not valid 118 // to set PlanOutPath without also setting PlanOutBackend. 119 diags = diags.Append(fmt.Errorf( 120 "PlanOutPath set without also setting PlanOutBackend (this is a bug in Terraform)"), 121 ) 122 op.ReportResult(runningOp, diags) 123 return 124 } 125 plan.Backend = *op.PlanOutBackend 126 127 // We may have updated the state in the refresh step above, but we 128 // will freeze that updated state in the plan file for now and 129 // only write it if this plan is subsequently applied. 130 plannedStateFile := statemgr.PlannedStateUpdate(opState, plan.PriorState) 131 132 // We also include a file containing the state as it existed before 133 // we took any action at all, but this one isn't intended to ever 134 // be saved to the backend (an equivalent snapshot should already be 135 // there) and so we just use a stub state file header in this case. 136 // NOTE: This won't be exactly identical to the latest state snapshot 137 // in the backend because it's still been subject to state upgrading 138 // to make it consumable by the current Terraform version, and 139 // intentionally doesn't preserve the header info. 140 prevStateFile := &statefile.File{ 141 State: plan.PrevRunState, 142 } 143 144 log.Printf("[INFO] backend/local: writing plan output to: %s", path) 145 err := planfile.Create(path, planfile.CreateArgs{ 146 ConfigSnapshot: configSnap, 147 PreviousRunStateFile: prevStateFile, 148 StateFile: plannedStateFile, 149 Plan: plan, 150 DependencyLocks: op.DependencyLocks, 151 }) 152 if err != nil { 153 diags = diags.Append(tfdiags.Sourceless( 154 tfdiags.Error, 155 "Failed to write plan file", 156 fmt.Sprintf("The plan file could not be written: %s.", err), 157 )) 158 op.ReportResult(runningOp, diags) 159 return 160 } 161 } 162 163 // Render the plan, if we produced one. 164 // (This might potentially be a partial plan with Errored set to true) 165 schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState) 166 diags = diags.Append(moreDiags) 167 if moreDiags.HasErrors() { 168 op.ReportResult(runningOp, diags) 169 return 170 } 171 op.View.Plan(plan, schemas) 172 173 // If we've accumulated any diagnostics along the way then we'll show them 174 // here just before we show the summary and next steps. This can potentially 175 // include errors, because we intentionally try to show a partial plan 176 // above even if Terraform Core encountered an error partway through 177 // creating it. 178 op.ReportResult(runningOp, diags) 179 180 if !runningOp.PlanEmpty { 181 op.View.PlanNextStep(op.PlanOutPath) 182 } 183 }