github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/terraform/context_plan.go (about) 1 package terraform 2 3 import ( 4 "fmt" 5 "log" 6 7 "github.com/zclconf/go-cty/cty" 8 9 "github.com/hashicorp/terraform/internal/addrs" 10 "github.com/hashicorp/terraform/internal/configs" 11 "github.com/hashicorp/terraform/internal/instances" 12 "github.com/hashicorp/terraform/internal/plans" 13 "github.com/hashicorp/terraform/internal/refactoring" 14 "github.com/hashicorp/terraform/internal/states" 15 "github.com/hashicorp/terraform/internal/tfdiags" 16 ) 17 18 // PlanOpts are the various options that affect the details of how Terraform 19 // will build a plan. 20 type PlanOpts struct { 21 Mode plans.Mode 22 SkipRefresh bool 23 SetVariables InputValues 24 Targets []addrs.Targetable 25 ForceReplace []addrs.AbsResourceInstance 26 } 27 28 // Plan generates an execution plan for the given context, and returns the 29 // refreshed state. 30 // 31 // The execution plan encapsulates the context and can be stored 32 // in order to reinstantiate a context later for Apply. 33 // 34 // Plan also updates the diff of this context to be the diff generated 35 // by the plan, so Apply can be called after. 36 func (c *Context) Plan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { 37 defer c.acquireRun("plan")() 38 var diags tfdiags.Diagnostics 39 40 // Save the downstream functions from needing to deal with these broken situations. 41 // No real callers should rely on these, but we have a bunch of old and 42 // sloppy tests that don't always populate arguments properly. 43 if config == nil { 44 config = configs.NewEmptyConfig() 45 } 46 if prevRunState == nil { 47 prevRunState = states.NewState() 48 } 49 if opts == nil { 50 opts = &PlanOpts{ 51 Mode: plans.NormalMode, 52 } 53 } 54 55 moreDiags := CheckCoreVersionRequirements(config) 56 diags = diags.Append(moreDiags) 57 // If version constraints are not met then we'll bail early since otherwise 58 // we're likely to just see a bunch of other errors related to 59 // incompatibilities, which could be overwhelming for the user. 60 if diags.HasErrors() { 61 return nil, diags 62 } 63 64 switch opts.Mode { 65 case plans.NormalMode, plans.DestroyMode: 66 // OK 67 case plans.RefreshOnlyMode: 68 if opts.SkipRefresh { 69 // The CLI layer (and other similar callers) should prevent this 70 // combination of options. 71 diags = diags.Append(tfdiags.Sourceless( 72 tfdiags.Error, 73 "Incompatible plan options", 74 "Cannot skip refreshing in refresh-only mode. This is a bug in Terraform.", 75 )) 76 return nil, diags 77 } 78 default: 79 // The CLI layer (and other similar callers) should not try to 80 // create a context for a mode that Terraform Core doesn't support. 81 diags = diags.Append(tfdiags.Sourceless( 82 tfdiags.Error, 83 "Unsupported plan mode", 84 fmt.Sprintf("Terraform Core doesn't know how to handle plan mode %s. This is a bug in Terraform.", opts.Mode), 85 )) 86 return nil, diags 87 } 88 if len(opts.ForceReplace) > 0 && opts.Mode != plans.NormalMode { 89 // The other modes don't generate no-op or update actions that we might 90 // upgrade to be "replace", so doesn't make sense to combine those. 91 diags = diags.Append(tfdiags.Sourceless( 92 tfdiags.Error, 93 "Unsupported plan mode", 94 "Forcing resource instance replacement (with -replace=...) is allowed only in normal planning mode.", 95 )) 96 return nil, diags 97 } 98 99 variables := mergeDefaultInputVariableValues(opts.SetVariables, config.Module.Variables) 100 101 // By the time we get here, we should have values defined for all of 102 // the root module variables, even if some of them are "unknown". It's the 103 // caller's responsibility to have already handled the decoding of these 104 // from the various ways the CLI allows them to be set and to produce 105 // user-friendly error messages if they are not all present, and so 106 // the error message from checkInputVariables should never be seen and 107 // includes language asking the user to report a bug. 108 varDiags := checkInputVariables(config.Module.Variables, variables) 109 diags = diags.Append(varDiags) 110 111 if len(opts.Targets) > 0 { 112 diags = diags.Append(tfdiags.Sourceless( 113 tfdiags.Warning, 114 "Resource targeting is in effect", 115 `You are creating a plan with the -target option, which means that the result of this plan may not represent all of the changes requested by the current configuration. 116 117 The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when Terraform specifically suggests to use it as part of an error message.`, 118 )) 119 } 120 121 var plan *plans.Plan 122 var planDiags tfdiags.Diagnostics 123 switch opts.Mode { 124 case plans.NormalMode: 125 plan, planDiags = c.plan(config, prevRunState, variables, opts) 126 case plans.DestroyMode: 127 plan, planDiags = c.destroyPlan(config, prevRunState, variables, opts) 128 case plans.RefreshOnlyMode: 129 plan, planDiags = c.refreshOnlyPlan(config, prevRunState, variables, opts) 130 default: 131 panic(fmt.Sprintf("unsupported plan mode %s", opts.Mode)) 132 } 133 diags = diags.Append(planDiags) 134 if diags.HasErrors() { 135 return nil, diags 136 } 137 138 // convert the variables into the format expected for the plan 139 varVals := make(map[string]plans.DynamicValue, len(variables)) 140 for k, iv := range variables { 141 // We use cty.DynamicPseudoType here so that we'll save both the 142 // value _and_ its dynamic type in the plan, so we can recover 143 // exactly the same value later. 144 dv, err := plans.NewDynamicValue(iv.Value, cty.DynamicPseudoType) 145 if err != nil { 146 diags = diags.Append(tfdiags.Sourceless( 147 tfdiags.Error, 148 "Failed to prepare variable value for plan", 149 fmt.Sprintf("The value for variable %q could not be serialized to store in the plan: %s.", k, err), 150 )) 151 continue 152 } 153 varVals[k] = dv 154 } 155 156 // insert the run-specific data from the context into the plan; variables, 157 // targets and provider SHAs. 158 if plan != nil { 159 plan.VariableValues = varVals 160 plan.TargetAddrs = opts.Targets 161 plan.ProviderSHA256s = c.providerSHA256s 162 } else if !diags.HasErrors() { 163 panic("nil plan but no errors") 164 } 165 166 return plan, diags 167 } 168 169 var DefaultPlanOpts = &PlanOpts{ 170 Mode: plans.NormalMode, 171 } 172 173 func (c *Context) plan(config *configs.Config, prevRunState *states.State, rootVariables InputValues, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { 174 var diags tfdiags.Diagnostics 175 176 if opts.Mode != plans.NormalMode { 177 panic(fmt.Sprintf("called Context.plan with %s", opts.Mode)) 178 } 179 180 plan, walkDiags := c.planWalk(config, prevRunState, rootVariables, opts) 181 diags = diags.Append(walkDiags) 182 if diags.HasErrors() { 183 return nil, diags 184 } 185 186 // The refreshed state ends up with some placeholder objects in it for 187 // objects pending creation. We only really care about those being in 188 // the working state, since that's what we're going to use when applying, 189 // so we'll prune them all here. 190 plan.PriorState.SyncWrapper().RemovePlannedResourceInstanceObjects() 191 192 return plan, diags 193 } 194 195 func (c *Context) refreshOnlyPlan(config *configs.Config, prevRunState *states.State, rootVariables InputValues, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { 196 var diags tfdiags.Diagnostics 197 198 if opts.Mode != plans.RefreshOnlyMode { 199 panic(fmt.Sprintf("called Context.refreshOnlyPlan with %s", opts.Mode)) 200 } 201 202 plan, walkDiags := c.planWalk(config, prevRunState, rootVariables, opts) 203 diags = diags.Append(walkDiags) 204 if diags.HasErrors() { 205 return nil, diags 206 } 207 208 // If the graph builder and graph nodes correctly obeyed our directive 209 // to refresh only, the set of resource changes should always be empty. 210 // We'll safety-check that here so we can return a clear message about it, 211 // rather than probably just generating confusing output at the UI layer. 212 if len(plan.Changes.Resources) != 0 { 213 // Some extra context in the logs in case the user reports this message 214 // as a bug, as a starting point for debugging. 215 for _, rc := range plan.Changes.Resources { 216 if depKey := rc.DeposedKey; depKey == states.NotDeposed { 217 log.Printf("[DEBUG] Refresh-only plan includes %s change for %s", rc.Action, rc.Addr) 218 } else { 219 log.Printf("[DEBUG] Refresh-only plan includes %s change for %s deposed object %s", rc.Action, rc.Addr, depKey) 220 } 221 } 222 diags = diags.Append(tfdiags.Sourceless( 223 tfdiags.Error, 224 "Invalid refresh-only plan", 225 "Terraform generated planned resource changes in a refresh-only plan. This is a bug in Terraform.", 226 )) 227 } 228 229 // Prune out any placeholder objects we put in the state to represent 230 // objects that would need to be created. 231 plan.PriorState.SyncWrapper().RemovePlannedResourceInstanceObjects() 232 233 return plan, diags 234 } 235 236 func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State, rootVariables InputValues, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { 237 var diags tfdiags.Diagnostics 238 pendingPlan := &plans.Plan{} 239 240 if opts.Mode != plans.DestroyMode { 241 panic(fmt.Sprintf("called Context.destroyPlan with %s", opts.Mode)) 242 } 243 244 priorState := prevRunState 245 246 // A destroy plan starts by running Refresh to read any pending data 247 // sources, and remove missing managed resources. This is required because 248 // a "destroy plan" is only creating delete changes, and is essentially a 249 // local operation. 250 // 251 // NOTE: if skipRefresh _is_ set then we'll rely on the destroy-plan walk 252 // below to upgrade the prevRunState and priorState both to the latest 253 // resource type schemas, so NodePlanDestroyableResourceInstance.Execute 254 // must coordinate with this by taking that action only when c.skipRefresh 255 // _is_ set. This coupling between the two is unfortunate but necessary 256 // to work within our current structure. 257 if !opts.SkipRefresh { 258 log.Printf("[TRACE] Context.destroyPlan: calling Context.plan to get the effect of refreshing the prior state") 259 normalOpts := *opts 260 normalOpts.Mode = plans.NormalMode 261 refreshPlan, refreshDiags := c.plan(config, prevRunState, rootVariables, &normalOpts) 262 diags = diags.Append(refreshDiags) 263 if diags.HasErrors() { 264 return nil, diags 265 } 266 267 // insert the refreshed state into the destroy plan result, and ignore 268 // the changes recorded from the refresh. 269 pendingPlan.PriorState = refreshPlan.PriorState.DeepCopy() 270 pendingPlan.PrevRunState = refreshPlan.PrevRunState.DeepCopy() 271 log.Printf("[TRACE] Context.destroyPlan: now _really_ creating a destroy plan") 272 273 // We'll use the refreshed state -- which is the "prior state" from 274 // the perspective of this "pending plan" -- as the starting state 275 // for our destroy-plan walk, so it can take into account if we 276 // detected during refreshing that anything was already deleted outside 277 // of Terraform. 278 priorState = pendingPlan.PriorState 279 } 280 281 destroyPlan, walkDiags := c.planWalk(config, priorState, rootVariables, opts) 282 diags = diags.Append(walkDiags) 283 if walkDiags.HasErrors() { 284 return nil, diags 285 } 286 287 if !opts.SkipRefresh { 288 // If we didn't skip refreshing then we want the previous run state 289 // prior state to be the one we originally fed into the c.plan call 290 // above, not the refreshed version we used for the destroy walk. 291 destroyPlan.PrevRunState = pendingPlan.PrevRunState 292 } 293 294 return destroyPlan, diags 295 } 296 297 func (c *Context) prePlanFindAndApplyMoves(config *configs.Config, prevRunState *states.State, targets []addrs.Targetable) ([]refactoring.MoveStatement, map[addrs.UniqueKey]refactoring.MoveResult) { 298 moveStmts := refactoring.FindMoveStatements(config) 299 moveResults := refactoring.ApplyMoves(moveStmts, prevRunState) 300 if len(targets) > 0 { 301 for _, result := range moveResults { 302 matchesTarget := false 303 for _, targetAddr := range targets { 304 if targetAddr.TargetContains(result.From) { 305 matchesTarget = true 306 break 307 } 308 } 309 //lint:ignore SA9003 TODO 310 if !matchesTarget { 311 // TODO: Return an error stating that a targeted plan is 312 // only valid if it includes this address that was moved. 313 } 314 } 315 } 316 return moveStmts, moveResults 317 } 318 319 func (c *Context) postPlanValidateMoves(config *configs.Config, stmts []refactoring.MoveStatement, allInsts instances.Set) tfdiags.Diagnostics { 320 return refactoring.ValidateMoves(stmts, config, allInsts) 321 } 322 323 func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, rootVariables InputValues, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { 324 var diags tfdiags.Diagnostics 325 log.Printf("[DEBUG] Building and walking plan graph for %s", opts.Mode) 326 327 prevRunState = prevRunState.DeepCopy() // don't modify the caller's object when we process the moves 328 moveStmts, moveResults := c.prePlanFindAndApplyMoves(config, prevRunState, opts.Targets) 329 330 graph, walkOp, moreDiags := c.planGraph(config, prevRunState, opts, true) 331 diags = diags.Append(moreDiags) 332 if diags.HasErrors() { 333 return nil, diags 334 } 335 336 // If we get here then we should definitely have a non-nil "graph", which 337 // we can now walk. 338 changes := plans.NewChanges() 339 walker, walkDiags := c.walk(graph, walkOp, &graphWalkOpts{ 340 Config: config, 341 InputState: prevRunState, 342 Changes: changes, 343 MoveResults: moveResults, 344 RootVariableValues: rootVariables, 345 }) 346 diags = diags.Append(walker.NonFatalDiagnostics) 347 diags = diags.Append(walkDiags) 348 diags = diags.Append(c.postPlanValidateMoves(config, moveStmts, walker.InstanceExpander.AllInstances())) 349 350 plan := &plans.Plan{ 351 UIMode: opts.Mode, 352 Changes: changes, 353 PriorState: walker.RefreshState.Close(), 354 PrevRunState: walker.PrevRunState.Close(), 355 356 // Other fields get populated by Context.Plan after we return 357 } 358 return plan, diags 359 } 360 361 func (c *Context) planGraph(config *configs.Config, prevRunState *states.State, opts *PlanOpts, validate bool) (*Graph, walkOperation, tfdiags.Diagnostics) { 362 switch mode := opts.Mode; mode { 363 case plans.NormalMode: 364 graph, diags := (&PlanGraphBuilder{ 365 Config: config, 366 State: prevRunState, 367 Plugins: c.plugins, 368 Targets: opts.Targets, 369 ForceReplace: opts.ForceReplace, 370 Validate: validate, 371 skipRefresh: opts.SkipRefresh, 372 }).Build(addrs.RootModuleInstance) 373 return graph, walkPlan, diags 374 case plans.RefreshOnlyMode: 375 graph, diags := (&PlanGraphBuilder{ 376 Config: config, 377 State: prevRunState, 378 Plugins: c.plugins, 379 Targets: opts.Targets, 380 Validate: validate, 381 skipRefresh: opts.SkipRefresh, 382 skipPlanChanges: true, // this activates "refresh only" mode. 383 }).Build(addrs.RootModuleInstance) 384 return graph, walkPlan, diags 385 case plans.DestroyMode: 386 graph, diags := (&DestroyPlanGraphBuilder{ 387 Config: config, 388 State: prevRunState, 389 Plugins: c.plugins, 390 Targets: opts.Targets, 391 Validate: validate, 392 skipRefresh: opts.SkipRefresh, 393 }).Build(addrs.RootModuleInstance) 394 return graph, walkPlanDestroy, diags 395 default: 396 // The above should cover all plans.Mode values 397 panic(fmt.Sprintf("unsupported plan mode %s", mode)) 398 } 399 } 400 401 // PlanGraphForUI is a last vestage of graphs in the public interface of Context 402 // (as opposed to graphs as an implementation detail) intended only for use 403 // by the "terraform graph" command when asked to render a plan-time graph. 404 // 405 // The result of this is intended only for rendering ot the user as a dot 406 // graph, and so may change in future in order to make the result more useful 407 // in that context, even if drifts away from the physical graph that Terraform 408 // Core currently uses as an implementation detail of planning. 409 func (c *Context) PlanGraphForUI(config *configs.Config, prevRunState *states.State, mode plans.Mode) (*Graph, tfdiags.Diagnostics) { 410 // For now though, this really is just the internal graph, confusing 411 // implementation details and all. 412 413 var diags tfdiags.Diagnostics 414 415 opts := &PlanOpts{Mode: mode} 416 417 graph, _, moreDiags := c.planGraph(config, prevRunState, opts, false) 418 diags = diags.Append(moreDiags) 419 return graph, diags 420 }