github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/local/backend_plan.go (about) 1 package local 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "log" 8 "sort" 9 "strings" 10 11 "github.com/mitchellh/cli" 12 "github.com/mitchellh/colorstring" 13 14 "github.com/hashicorp/terraform/addrs" 15 "github.com/hashicorp/terraform/backend" 16 "github.com/hashicorp/terraform/command/format" 17 "github.com/hashicorp/terraform/plans" 18 "github.com/hashicorp/terraform/plans/planfile" 19 "github.com/hashicorp/terraform/states" 20 "github.com/hashicorp/terraform/states/statemgr" 21 "github.com/hashicorp/terraform/terraform" 22 "github.com/hashicorp/terraform/tfdiags" 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 b.ReportResult(runningOp, diags) 43 return 44 } 45 46 // Local planning requires a config, unless we're planning to destroy. 47 if !op.Destroy && !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 Terraform configuration file (.tf file) and try again.", 55 )) 56 b.ReportResult(runningOp, diags) 57 return 58 } 59 60 // Setup our count hook that keeps track of resource changes 61 countHook := new(CountHook) 62 if b.ContextOpts == nil { 63 b.ContextOpts = new(terraform.ContextOpts) 64 } 65 old := b.ContextOpts.Hooks 66 defer func() { b.ContextOpts.Hooks = old }() 67 b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook) 68 69 // Get our context 70 tfCtx, configSnap, opState, ctxDiags := b.context(op) 71 diags = diags.Append(ctxDiags) 72 if ctxDiags.HasErrors() { 73 b.ReportResult(runningOp, diags) 74 return 75 } 76 77 // Setup the state 78 runningOp.State = tfCtx.State() 79 80 // If we're refreshing before plan, perform that 81 baseState := runningOp.State 82 if op.PlanRefresh { 83 log.Printf("[INFO] backend/local: plan calling Refresh") 84 85 if b.CLI != nil { 86 b.CLI.Output(b.Colorize().Color(strings.TrimSpace(planRefreshing) + "\n")) 87 } 88 89 refreshedState, refreshDiags := tfCtx.Refresh() 90 diags = diags.Append(refreshDiags) 91 if diags.HasErrors() { 92 b.ReportResult(runningOp, diags) 93 return 94 } 95 baseState = refreshedState // plan will be relative to our refreshed state 96 if b.CLI != nil { 97 b.CLI.Output("\n------------------------------------------------------------------------") 98 } 99 } 100 101 // Perform the plan in a goroutine so we can be interrupted 102 var plan *plans.Plan 103 var planDiags tfdiags.Diagnostics 104 doneCh := make(chan struct{}) 105 go func() { 106 defer close(doneCh) 107 log.Printf("[INFO] backend/local: plan calling Plan") 108 plan, planDiags = tfCtx.Plan() 109 }() 110 111 if b.opWait(doneCh, stopCtx, cancelCtx, tfCtx, opState) { 112 // If we get in here then the operation was cancelled, which is always 113 // considered to be a failure. 114 log.Printf("[INFO] backend/local: plan operation was force-cancelled by interrupt") 115 runningOp.Result = backend.OperationFailure 116 return 117 } 118 log.Printf("[INFO] backend/local: plan operation completed") 119 120 diags = diags.Append(planDiags) 121 if planDiags.HasErrors() { 122 b.ReportResult(runningOp, diags) 123 return 124 } 125 // Record state 126 runningOp.PlanEmpty = plan.Changes.Empty() 127 128 // Save the plan to disk 129 if path := op.PlanOutPath; path != "" { 130 if op.PlanOutBackend == nil { 131 // This is always a bug in the operation caller; it's not valid 132 // to set PlanOutPath without also setting PlanOutBackend. 133 diags = diags.Append(fmt.Errorf( 134 "PlanOutPath set without also setting PlanOutBackend (this is a bug in Terraform)"), 135 ) 136 b.ReportResult(runningOp, diags) 137 return 138 } 139 plan.Backend = *op.PlanOutBackend 140 141 // We may have updated the state in the refresh step above, but we 142 // will freeze that updated state in the plan file for now and 143 // only write it if this plan is subsequently applied. 144 plannedStateFile := statemgr.PlannedStateUpdate(opState, baseState) 145 146 log.Printf("[INFO] backend/local: writing plan output to: %s", path) 147 err := planfile.Create(path, configSnap, plannedStateFile, plan) 148 if err != nil { 149 diags = diags.Append(tfdiags.Sourceless( 150 tfdiags.Error, 151 "Failed to write plan file", 152 fmt.Sprintf("The plan file could not be written: %s.", err), 153 )) 154 b.ReportResult(runningOp, diags) 155 return 156 } 157 } 158 159 // Perform some output tasks if we have a CLI to output to. 160 if b.CLI != nil { 161 schemas := tfCtx.Schemas() 162 163 if plan.Changes.Empty() { 164 b.CLI.Output("\n" + b.Colorize().Color(strings.TrimSpace(planNoChanges))) 165 // Even if there are no changes, there still could be some warnings 166 b.ShowDiagnostics(diags) 167 return 168 } 169 170 b.renderPlan(plan, baseState, schemas) 171 172 // If we've accumulated any warnings along the way then we'll show them 173 // here just before we show the summary and next steps. If we encountered 174 // errors then we would've returned early at some other point above. 175 b.ShowDiagnostics(diags) 176 177 // Give the user some next-steps, unless we're running in an automation 178 // tool which is presumed to provide its own UI for further actions. 179 if !b.RunningInAutomation { 180 181 b.CLI.Output("\n------------------------------------------------------------------------") 182 183 if path := op.PlanOutPath; path == "" { 184 b.CLI.Output(fmt.Sprintf( 185 "\n" + strings.TrimSpace(planHeaderNoOutput) + "\n", 186 )) 187 } else { 188 b.CLI.Output(fmt.Sprintf( 189 "\n"+strings.TrimSpace(planHeaderYesOutput)+"\n", 190 path, path, 191 )) 192 } 193 } 194 } 195 } 196 197 func (b *Local) renderPlan(plan *plans.Plan, state *states.State, schemas *terraform.Schemas) { 198 RenderPlan(plan, state, schemas, b.CLI, b.Colorize()) 199 } 200 201 // RenderPlan renders the given plan to the given UI. 202 // 203 // This is exported only so that the "terraform show" command can re-use it. 204 // Ideally it would be somewhere outside of this backend code so that both 205 // can call into it, but we're leaving it here for now in order to avoid 206 // disruptive refactoring. 207 // 208 // If you find yourself wanting to call this function from a third callsite, 209 // please consider whether it's time to do the more disruptive refactoring 210 // so that something other than the local backend package is offering this 211 // functionality. 212 func RenderPlan(plan *plans.Plan, state *states.State, schemas *terraform.Schemas, ui cli.Ui, colorize *colorstring.Colorize) { 213 counts := map[plans.Action]int{} 214 var rChanges []*plans.ResourceInstanceChangeSrc 215 for _, change := range plan.Changes.Resources { 216 if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode { 217 // Avoid rendering data sources on deletion 218 continue 219 } 220 221 rChanges = append(rChanges, change) 222 counts[change.Action]++ 223 } 224 225 headerBuf := &bytes.Buffer{} 226 fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(planHeaderIntro)) 227 if counts[plans.Create] > 0 { 228 fmt.Fprintf(headerBuf, "%s create\n", format.DiffActionSymbol(plans.Create)) 229 } 230 if counts[plans.Update] > 0 { 231 fmt.Fprintf(headerBuf, "%s update in-place\n", format.DiffActionSymbol(plans.Update)) 232 } 233 if counts[plans.Delete] > 0 { 234 fmt.Fprintf(headerBuf, "%s destroy\n", format.DiffActionSymbol(plans.Delete)) 235 } 236 if counts[plans.DeleteThenCreate] > 0 { 237 fmt.Fprintf(headerBuf, "%s destroy and then create replacement\n", format.DiffActionSymbol(plans.DeleteThenCreate)) 238 } 239 if counts[plans.CreateThenDelete] > 0 { 240 fmt.Fprintf(headerBuf, "%s create replacement and then destroy\n", format.DiffActionSymbol(plans.CreateThenDelete)) 241 } 242 if counts[plans.Read] > 0 { 243 fmt.Fprintf(headerBuf, "%s read (data resources)\n", format.DiffActionSymbol(plans.Read)) 244 } 245 246 ui.Output(colorize.Color(headerBuf.String())) 247 248 ui.Output("Terraform will perform the following actions:\n") 249 250 // Note: we're modifying the backing slice of this plan object in-place 251 // here. The ordering of resource changes in a plan is not significant, 252 // but we can only do this safely here because we can assume that nobody 253 // is concurrently modifying our changes while we're trying to print it. 254 sort.Slice(rChanges, func(i, j int) bool { 255 iA := rChanges[i].Addr 256 jA := rChanges[j].Addr 257 if iA.String() == jA.String() { 258 return rChanges[i].DeposedKey < rChanges[j].DeposedKey 259 } 260 return iA.Less(jA) 261 }) 262 263 for _, rcs := range rChanges { 264 if rcs.Action == plans.NoOp { 265 continue 266 } 267 providerSchema := schemas.ProviderSchema(rcs.ProviderAddr.ProviderConfig.Type.LegacyString()) 268 if providerSchema == nil { 269 // Should never happen 270 ui.Output(fmt.Sprintf("(schema missing for %s)\n", rcs.ProviderAddr)) 271 continue 272 } 273 rSchema, _ := providerSchema.SchemaForResourceAddr(rcs.Addr.Resource.Resource) 274 if rSchema == nil { 275 // Should never happen 276 ui.Output(fmt.Sprintf("(schema missing for %s)\n", rcs.Addr)) 277 continue 278 } 279 280 // check if the change is due to a tainted resource 281 tainted := false 282 if !state.Empty() { 283 if is := state.ResourceInstance(rcs.Addr); is != nil { 284 if obj := is.GetGeneration(rcs.DeposedKey.Generation()); obj != nil { 285 tainted = obj.Status == states.ObjectTainted 286 } 287 } 288 } 289 290 ui.Output(format.ResourceChange( 291 rcs, 292 tainted, 293 rSchema, 294 colorize, 295 )) 296 } 297 298 // stats is similar to counts above, but: 299 // - it considers only resource changes 300 // - it simplifies "replace" into both a create and a delete 301 stats := map[plans.Action]int{} 302 for _, change := range rChanges { 303 switch change.Action { 304 case plans.CreateThenDelete, plans.DeleteThenCreate: 305 stats[plans.Create]++ 306 stats[plans.Delete]++ 307 default: 308 stats[change.Action]++ 309 } 310 } 311 ui.Output(colorize.Color(fmt.Sprintf( 312 "[reset][bold]Plan:[reset] "+ 313 "%d to add, %d to change, %d to destroy.", 314 stats[plans.Create], stats[plans.Update], stats[plans.Delete], 315 ))) 316 } 317 318 const planHeaderIntro = ` 319 An execution plan has been generated and is shown below. 320 Resource actions are indicated with the following symbols: 321 ` 322 323 const planHeaderNoOutput = ` 324 Note: You didn't specify an "-out" parameter to save this plan, so Terraform 325 can't guarantee that exactly these actions will be performed if 326 "terraform apply" is subsequently run. 327 ` 328 329 const planHeaderYesOutput = ` 330 This plan was saved to: %s 331 332 To perform exactly these actions, run the following command to apply: 333 terraform apply %q 334 ` 335 336 const planNoChanges = ` 337 [reset][bold][green]No changes. Infrastructure is up-to-date.[reset][green] 338 339 This means that Terraform did not detect any differences between your 340 configuration and real physical resources that exist. As a result, no 341 actions need to be performed. 342 ` 343 344 const planRefreshing = ` 345 [reset][bold]Refreshing Terraform state in-memory prior to plan...[reset] 346 The refreshed state will be used to calculate this plan, but will not be 347 persisted to local or remote state storage. 348 `