github.com/opentofu/opentofu@v1.7.1/internal/backend/local/backend_plan.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package local 7 8 import ( 9 "context" 10 "fmt" 11 "io" 12 "log" 13 14 "github.com/opentofu/opentofu/internal/backend" 15 "github.com/opentofu/opentofu/internal/genconfig" 16 "github.com/opentofu/opentofu/internal/logging" 17 "github.com/opentofu/opentofu/internal/plans" 18 "github.com/opentofu/opentofu/internal/plans/planfile" 19 "github.com/opentofu/opentofu/internal/states/statefile" 20 "github.com/opentofu/opentofu/internal/states/statemgr" 21 "github.com/opentofu/opentofu/internal/tfdiags" 22 "github.com/opentofu/opentofu/internal/tofu" 23 ) 24 25 func (b *Local) opPlan( 26 stopCtx context.Context, 27 cancelCtx context.Context, 28 op *backend.Operation, 29 runningOp *backend.RunningOperation) { 30 31 log.Printf("[INFO] backend/local: starting Plan operation") 32 33 var diags tfdiags.Diagnostics 34 35 if op.PlanFile != nil { 36 diags = diags.Append(tfdiags.Sourceless( 37 tfdiags.Error, 38 "Can't re-plan a saved plan", 39 "The plan command was given a saved plan file as its input. This command generates "+ 40 "a new plan, and so it requires a configuration directory as its argument.", 41 )) 42 op.ReportResult(runningOp, diags) 43 return 44 } 45 46 // Local planning requires a config, unless we're planning to destroy. 47 if op.PlanMode != plans.DestroyMode && !op.HasConfig() { 48 diags = diags.Append(tfdiags.Sourceless( 49 tfdiags.Error, 50 "No configuration files", 51 "Plan requires configuration to be present. Planning without a configuration would "+ 52 "mark everything for destruction, which is normally not what is desired. If you "+ 53 "would like to destroy everything, run plan with the -destroy option. Otherwise, "+ 54 "create a OpenTofu configuration file (.tf file) and try again.", 55 )) 56 op.ReportResult(runningOp, diags) 57 return 58 } 59 60 if len(op.GenerateConfigOut) > 0 { 61 if op.PlanMode != plans.NormalMode { 62 diags = diags.Append(tfdiags.Sourceless( 63 tfdiags.Error, 64 "Invalid generate-config-out flag", 65 "Config can only be generated during a normal plan operation, and not during a refresh-only or destroy plan.")) 66 op.ReportResult(runningOp, diags) 67 return 68 } 69 70 diags = diags.Append(genconfig.ValidateTargetFile(op.GenerateConfigOut)) 71 if diags.HasErrors() { 72 op.ReportResult(runningOp, diags) 73 return 74 } 75 } 76 77 if b.ContextOpts == nil { 78 b.ContextOpts = new(tofu.ContextOpts) 79 } 80 81 // Get our context 82 lr, configSnap, opState, ctxDiags := b.localRun(op) 83 diags = diags.Append(ctxDiags) 84 if ctxDiags.HasErrors() { 85 op.ReportResult(runningOp, diags) 86 return 87 } 88 // the state was locked during succesfull context creation; unlock the state 89 // when the operation completes 90 defer func() { 91 diags := op.StateLocker.Unlock() 92 if diags.HasErrors() { 93 op.View.Diagnostics(diags) 94 runningOp.Result = backend.OperationFailure 95 } 96 }() 97 98 // Since planning doesn't immediately change the persisted state, the 99 // resulting state is always just the input state. 100 runningOp.State = lr.InputState 101 102 // Perform the plan in a goroutine so we can be interrupted 103 var plan *plans.Plan 104 var planDiags tfdiags.Diagnostics 105 doneCh := make(chan struct{}) 106 panicHandler := logging.PanicHandlerWithTraceFn() 107 go func() { 108 defer panicHandler() 109 defer close(doneCh) 110 log.Printf("[INFO] backend/local: plan calling Plan") 111 plan, planDiags = lr.Core.Plan(lr.Config, lr.InputState, lr.PlanOpts) 112 }() 113 114 if b.opWait(doneCh, stopCtx, cancelCtx, lr.Core, opState, op.View) { 115 // If we get in here then the operation was cancelled, which is always 116 // considered to be a failure. 117 log.Printf("[INFO] backend/local: plan operation was force-cancelled by interrupt") 118 runningOp.Result = backend.OperationFailure 119 return 120 } 121 log.Printf("[INFO] backend/local: plan operation completed") 122 123 // NOTE: We intentionally don't stop here on errors because we always want 124 // to try to present a partial plan report and, if the user chose to, 125 // generate a partial saved plan file for external analysis. 126 diags = diags.Append(planDiags) 127 128 // Even if there are errors we need to handle anything that may be 129 // contained within the plan, so only exit if there is no data at all. 130 if plan == nil { 131 runningOp.PlanEmpty = true 132 op.ReportResult(runningOp, diags) 133 return 134 } 135 136 // Record whether this plan includes any side-effects that could be applied. 137 runningOp.PlanEmpty = !plan.CanApply() 138 139 // Save the plan to disk 140 if path := op.PlanOutPath; path != "" { 141 if op.PlanOutBackend == nil { 142 // This is always a bug in the operation caller; it's not valid 143 // to set PlanOutPath without also setting PlanOutBackend. 144 diags = diags.Append(fmt.Errorf( 145 "PlanOutPath set without also setting PlanOutBackend (this is a bug in OpenTofu)"), 146 ) 147 op.ReportResult(runningOp, diags) 148 return 149 } 150 plan.Backend = *op.PlanOutBackend 151 152 // We may have updated the state in the refresh step above, but we 153 // will freeze that updated state in the plan file for now and 154 // only write it if this plan is subsequently applied. 155 plannedStateFile := statemgr.PlannedStateUpdate(opState, plan.PriorState) 156 157 // We also include a file containing the state as it existed before 158 // we took any action at all, but this one isn't intended to ever 159 // be saved to the backend (an equivalent snapshot should already be 160 // there) and so we just use a stub state file header in this case. 161 // NOTE: This won't be exactly identical to the latest state snapshot 162 // in the backend because it's still been subject to state upgrading 163 // to make it consumable by the current OpenTofu version, and 164 // intentionally doesn't preserve the header info. 165 prevStateFile := &statefile.File{ 166 State: plan.PrevRunState, 167 } 168 169 log.Printf("[INFO] backend/local: writing plan output to: %s", path) 170 err := planfile.Create(path, planfile.CreateArgs{ 171 ConfigSnapshot: configSnap, 172 PreviousRunStateFile: prevStateFile, 173 StateFile: plannedStateFile, 174 Plan: plan, 175 DependencyLocks: op.DependencyLocks, 176 }, op.Encryption.Plan()) 177 if err != nil { 178 diags = diags.Append(tfdiags.Sourceless( 179 tfdiags.Error, 180 "Failed to write plan file", 181 fmt.Sprintf("The plan file could not be written: %s.", err), 182 )) 183 op.ReportResult(runningOp, diags) 184 return 185 } 186 } 187 188 // Render the plan, if we produced one. 189 // (This might potentially be a partial plan with Errored set to true) 190 schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState) 191 diags = diags.Append(moreDiags) 192 if moreDiags.HasErrors() { 193 op.ReportResult(runningOp, diags) 194 return 195 } 196 197 // Write out any generated config, before we render the plan. 198 wroteConfig, moreDiags := maybeWriteGeneratedConfig(plan, op.GenerateConfigOut) 199 diags = diags.Append(moreDiags) 200 if moreDiags.HasErrors() { 201 op.ReportResult(runningOp, diags) 202 return 203 } 204 205 op.View.Plan(plan, schemas) 206 207 // If we've accumulated any diagnostics along the way then we'll show them 208 // here just before we show the summary and next steps. This can potentially 209 // include errors, because we intentionally try to show a partial plan 210 // above even if OpenTofu Core encountered an error partway through 211 // creating it. 212 op.ReportResult(runningOp, diags) 213 214 if !runningOp.PlanEmpty { 215 if wroteConfig { 216 op.View.PlanNextStep(op.PlanOutPath, op.GenerateConfigOut) 217 } else { 218 op.View.PlanNextStep(op.PlanOutPath, "") 219 } 220 } 221 } 222 223 func maybeWriteGeneratedConfig(plan *plans.Plan, out string) (wroteConfig bool, diags tfdiags.Diagnostics) { 224 if genconfig.ShouldWriteConfig(out) { 225 diags := genconfig.ValidateTargetFile(out) 226 if diags.HasErrors() { 227 return false, diags 228 } 229 230 var writer io.Writer 231 for _, c := range plan.Changes.Resources { 232 change := genconfig.Change{ 233 Addr: c.Addr.String(), 234 GeneratedConfig: c.GeneratedConfig, 235 } 236 if c.Importing != nil { 237 change.ImportID = c.Importing.ID 238 } 239 240 var moreDiags tfdiags.Diagnostics 241 writer, wroteConfig, moreDiags = change.MaybeWriteConfig(writer, out) 242 if moreDiags.HasErrors() { 243 return false, diags.Append(moreDiags) 244 } 245 } 246 } 247 248 if wroteConfig { 249 diags = diags.Append(tfdiags.Sourceless( 250 tfdiags.Warning, 251 "Config generation is experimental", 252 "Generating configuration during import is currently experimental, and the generated configuration format may change in future versions.")) 253 } 254 255 return wroteConfig, diags 256 }