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