github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/backend/local/backend_plan.go (about) 1 package local 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 8 "github.com/iaas-resource-provision/iaas-rpc/internal/backend" 9 "github.com/iaas-resource-provision/iaas-rpc/internal/plans" 10 "github.com/iaas-resource-provision/iaas-rpc/internal/plans/planfile" 11 "github.com/iaas-resource-provision/iaas-rpc/internal/states/statefile" 12 "github.com/iaas-resource-provision/iaas-rpc/internal/states/statemgr" 13 "github.com/iaas-resource-provision/iaas-rpc/internal/terraform" 14 "github.com/iaas-resource-provision/iaas-rpc/internal/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 tfCtx, configSnap, opState, ctxDiags := b.context(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 runningOp.State = tfCtx.State() 74 75 // Perform the plan in a goroutine so we can be interrupted 76 var plan *plans.Plan 77 var planDiags tfdiags.Diagnostics 78 doneCh := make(chan struct{}) 79 go func() { 80 defer close(doneCh) 81 log.Printf("[INFO] backend/local: plan calling Plan") 82 plan, planDiags = tfCtx.Plan() 83 }() 84 85 if b.opWait(doneCh, stopCtx, cancelCtx, tfCtx, opState, op.View) { 86 // If we get in here then the operation was cancelled, which is always 87 // considered to be a failure. 88 log.Printf("[INFO] backend/local: plan operation was force-cancelled by interrupt") 89 runningOp.Result = backend.OperationFailure 90 return 91 } 92 log.Printf("[INFO] backend/local: plan operation completed") 93 94 diags = diags.Append(planDiags) 95 if planDiags.HasErrors() { 96 op.ReportResult(runningOp, diags) 97 return 98 } 99 100 // Record whether this plan includes any side-effects that could be applied. 101 runningOp.PlanEmpty = !plan.CanApply() 102 103 // Save the plan to disk 104 if path := op.PlanOutPath; path != "" { 105 if op.PlanOutBackend == nil { 106 // This is always a bug in the operation caller; it's not valid 107 // to set PlanOutPath without also setting PlanOutBackend. 108 diags = diags.Append(fmt.Errorf( 109 "PlanOutPath set without also setting PlanOutBackend (this is a bug in Terraform)"), 110 ) 111 op.ReportResult(runningOp, diags) 112 return 113 } 114 plan.Backend = *op.PlanOutBackend 115 116 // We may have updated the state in the refresh step above, but we 117 // will freeze that updated state in the plan file for now and 118 // only write it if this plan is subsequently applied. 119 plannedStateFile := statemgr.PlannedStateUpdate(opState, plan.PriorState) 120 121 // We also include a file containing the state as it existed before 122 // we took any action at all, but this one isn't intended to ever 123 // be saved to the backend (an equivalent snapshot should already be 124 // there) and so we just use a stub state file header in this case. 125 // NOTE: This won't be exactly identical to the latest state snapshot 126 // in the backend because it's still been subject to state upgrading 127 // to make it consumable by the current Terraform version, and 128 // intentionally doesn't preserve the header info. 129 prevStateFile := &statefile.File{ 130 State: plan.PrevRunState, 131 } 132 133 log.Printf("[INFO] backend/local: writing plan output to: %s", path) 134 err := planfile.Create(path, configSnap, prevStateFile, plannedStateFile, plan) 135 if err != nil { 136 diags = diags.Append(tfdiags.Sourceless( 137 tfdiags.Error, 138 "Failed to write plan file", 139 fmt.Sprintf("The plan file could not be written: %s.", err), 140 )) 141 op.ReportResult(runningOp, diags) 142 return 143 } 144 } 145 146 // Render the plan 147 op.View.Plan(plan, tfCtx.Schemas()) 148 149 // If we've accumulated any warnings along the way then we'll show them 150 // here just before we show the summary and next steps. If we encountered 151 // errors then we would've returned early at some other point above. 152 op.View.Diagnostics(diags) 153 154 if !runningOp.PlanEmpty { 155 op.View.PlanNextStep(op.PlanOutPath) 156 } 157 }