github.com/hugorut/terraform@v1.1.3/src/backend/local/backend_plan.go (about) 1 package local 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 8 "github.com/hugorut/terraform/src/backend" 9 "github.com/hugorut/terraform/src/logging" 10 "github.com/hugorut/terraform/src/plans" 11 "github.com/hugorut/terraform/src/plans/planfile" 12 "github.com/hugorut/terraform/src/states/statefile" 13 "github.com/hugorut/terraform/src/states/statemgr" 14 "github.com/hugorut/terraform/src/terraform" 15 "github.com/hugorut/terraform/src/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 diags = diags.Append(planDiags) 99 if planDiags.HasErrors() { 100 op.ReportResult(runningOp, diags) 101 return 102 } 103 104 // Record whether this plan includes any side-effects that could be applied. 105 runningOp.PlanEmpty = !plan.CanApply() 106 107 // Save the plan to disk 108 if path := op.PlanOutPath; path != "" { 109 if op.PlanOutBackend == nil { 110 // This is always a bug in the operation caller; it's not valid 111 // to set PlanOutPath without also setting PlanOutBackend. 112 diags = diags.Append(fmt.Errorf( 113 "PlanOutPath set without also setting PlanOutBackend (this is a bug in Terraform)"), 114 ) 115 op.ReportResult(runningOp, diags) 116 return 117 } 118 plan.Backend = *op.PlanOutBackend 119 120 // We may have updated the state in the refresh step above, but we 121 // will freeze that updated state in the plan file for now and 122 // only write it if this plan is subsequently applied. 123 plannedStateFile := statemgr.PlannedStateUpdate(opState, plan.PriorState) 124 125 // We also include a file containing the state as it existed before 126 // we took any action at all, but this one isn't intended to ever 127 // be saved to the backend (an equivalent snapshot should already be 128 // there) and so we just use a stub state file header in this case. 129 // NOTE: This won't be exactly identical to the latest state snapshot 130 // in the backend because it's still been subject to state upgrading 131 // to make it consumable by the current Terraform version, and 132 // intentionally doesn't preserve the header info. 133 prevStateFile := &statefile.File{ 134 State: plan.PrevRunState, 135 } 136 137 log.Printf("[INFO] backend/local: writing plan output to: %s", path) 138 err := planfile.Create(path, planfile.CreateArgs{ 139 ConfigSnapshot: configSnap, 140 PreviousRunStateFile: prevStateFile, 141 StateFile: plannedStateFile, 142 Plan: plan, 143 DependencyLocks: op.DependencyLocks, 144 }) 145 if err != nil { 146 diags = diags.Append(tfdiags.Sourceless( 147 tfdiags.Error, 148 "Failed to write plan file", 149 fmt.Sprintf("The plan file could not be written: %s.", err), 150 )) 151 op.ReportResult(runningOp, diags) 152 return 153 } 154 } 155 156 // Render the plan 157 schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState) 158 diags = diags.Append(moreDiags) 159 if moreDiags.HasErrors() { 160 op.ReportResult(runningOp, diags) 161 return 162 } 163 op.View.Plan(plan, schemas) 164 165 // If we've accumulated any warnings along the way then we'll show them 166 // here just before we show the summary and next steps. If we encountered 167 // errors then we would've returned early at some other point above. 168 op.View.Diagnostics(diags) 169 170 if !runningOp.PlanEmpty { 171 op.View.PlanNextStep(op.PlanOutPath) 172 } 173 }