github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/durgaform/context_plan.go (about) 1 package durgaform 2 3 import ( 4 "bytes" 5 "fmt" 6 "log" 7 "sort" 8 "strings" 9 10 "github.com/zclconf/go-cty/cty" 11 12 "github.com/eliastor/durgaform/internal/addrs" 13 "github.com/eliastor/durgaform/internal/configs" 14 "github.com/eliastor/durgaform/internal/instances" 15 "github.com/eliastor/durgaform/internal/lang/globalref" 16 "github.com/eliastor/durgaform/internal/plans" 17 "github.com/eliastor/durgaform/internal/refactoring" 18 "github.com/eliastor/durgaform/internal/states" 19 "github.com/eliastor/durgaform/internal/tfdiags" 20 ) 21 22 // PlanOpts are the various options that affect the details of how Durgaform 23 // will build a plan. 24 type PlanOpts struct { 25 // Mode defines what variety of plan the caller wishes to create. 26 // Refer to the documentation of the plans.Mode type and its values 27 // for more information. 28 Mode plans.Mode 29 30 // SkipRefresh specifies to trust that the current values for managed 31 // resource instances in the prior state are accurate and to therefore 32 // disable the usual step of fetching updated values for each resource 33 // instance using its corresponding provider. 34 SkipRefresh bool 35 36 // SetVariables are the raw values for root module variables as provided 37 // by the user who is requesting the run, prior to any normalization or 38 // substitution of defaults. See the documentation for the InputValue 39 // type for more information on how to correctly populate this. 40 SetVariables InputValues 41 42 // If Targets has a non-zero length then it activates targeted planning 43 // mode, where Durgaform will take actions only for resource instances 44 // mentioned in this set and any other objects those resource instances 45 // depend on. 46 // 47 // Targeted planning mode is intended for exceptional use only, 48 // and so populating this field will cause Durgaform to generate extra 49 // warnings as part of the planning result. 50 Targets []addrs.Targetable 51 52 // ForceReplace is a set of resource instance addresses whose corresponding 53 // objects should be forced planned for replacement if the provider's 54 // plan would otherwise have been to either update the object in-place or 55 // to take no action on it at all. 56 // 57 // A typical use of this argument is to ask Durgaform to replace an object 58 // which the user has determined is somehow degraded (via information from 59 // outside of Durgaform), thereby hopefully replacing it with a 60 // fully-functional new object. 61 ForceReplace []addrs.AbsResourceInstance 62 } 63 64 // Plan generates an execution plan for the given context, and returns the 65 // refreshed state. 66 // 67 // The execution plan encapsulates the context and can be stored 68 // in order to reinstantiate a context later for Apply. 69 // 70 // Plan also updates the diff of this context to be the diff generated 71 // by the plan, so Apply can be called after. 72 func (c *Context) Plan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { 73 defer c.acquireRun("plan")() 74 var diags tfdiags.Diagnostics 75 76 // Save the downstream functions from needing to deal with these broken situations. 77 // No real callers should rely on these, but we have a bunch of old and 78 // sloppy tests that don't always populate arguments properly. 79 if config == nil { 80 config = configs.NewEmptyConfig() 81 } 82 if prevRunState == nil { 83 prevRunState = states.NewState() 84 } 85 if opts == nil { 86 opts = &PlanOpts{ 87 Mode: plans.NormalMode, 88 } 89 } 90 91 moreDiags := c.checkConfigDependencies(config) 92 diags = diags.Append(moreDiags) 93 // If required dependencies are not available then we'll bail early since 94 // otherwise we're likely to just see a bunch of other errors related to 95 // incompatibilities, which could be overwhelming for the user. 96 if diags.HasErrors() { 97 return nil, diags 98 } 99 100 switch opts.Mode { 101 case plans.NormalMode, plans.DestroyMode: 102 // OK 103 case plans.RefreshOnlyMode: 104 if opts.SkipRefresh { 105 // The CLI layer (and other similar callers) should prevent this 106 // combination of options. 107 diags = diags.Append(tfdiags.Sourceless( 108 tfdiags.Error, 109 "Incompatible plan options", 110 "Cannot skip refreshing in refresh-only mode. This is a bug in Durgaform.", 111 )) 112 return nil, diags 113 } 114 default: 115 // The CLI layer (and other similar callers) should not try to 116 // create a context for a mode that Durgaform Core doesn't support. 117 diags = diags.Append(tfdiags.Sourceless( 118 tfdiags.Error, 119 "Unsupported plan mode", 120 fmt.Sprintf("Durgaform Core doesn't know how to handle plan mode %s. This is a bug in Terraform.", opts.Mode), 121 )) 122 return nil, diags 123 } 124 if len(opts.ForceReplace) > 0 && opts.Mode != plans.NormalMode { 125 // The other modes don't generate no-op or update actions that we might 126 // upgrade to be "replace", so doesn't make sense to combine those. 127 diags = diags.Append(tfdiags.Sourceless( 128 tfdiags.Error, 129 "Unsupported plan mode", 130 "Forcing resource instance replacement (with -replace=...) is allowed only in normal planning mode.", 131 )) 132 return nil, diags 133 } 134 135 // By the time we get here, we should have values defined for all of 136 // the root module variables, even if some of them are "unknown". It's the 137 // caller's responsibility to have already handled the decoding of these 138 // from the various ways the CLI allows them to be set and to produce 139 // user-friendly error messages if they are not all present, and so 140 // the error message from checkInputVariables should never be seen and 141 // includes language asking the user to report a bug. 142 varDiags := checkInputVariables(config.Module.Variables, opts.SetVariables) 143 diags = diags.Append(varDiags) 144 145 if len(opts.Targets) > 0 { 146 diags = diags.Append(tfdiags.Sourceless( 147 tfdiags.Warning, 148 "Resource targeting is in effect", 149 `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. 150 151 The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when Durgaform specifically suggests to use it as part of an error message.`, 152 )) 153 } 154 155 var plan *plans.Plan 156 var planDiags tfdiags.Diagnostics 157 switch opts.Mode { 158 case plans.NormalMode: 159 plan, planDiags = c.plan(config, prevRunState, opts) 160 case plans.DestroyMode: 161 plan, planDiags = c.destroyPlan(config, prevRunState, opts) 162 case plans.RefreshOnlyMode: 163 plan, planDiags = c.refreshOnlyPlan(config, prevRunState, opts) 164 default: 165 panic(fmt.Sprintf("unsupported plan mode %s", opts.Mode)) 166 } 167 diags = diags.Append(planDiags) 168 if diags.HasErrors() { 169 return nil, diags 170 } 171 172 // convert the variables into the format expected for the plan 173 varVals := make(map[string]plans.DynamicValue, len(opts.SetVariables)) 174 for k, iv := range opts.SetVariables { 175 if iv.Value == cty.NilVal { 176 continue // We only record values that the caller actually set 177 } 178 179 // We use cty.DynamicPseudoType here so that we'll save both the 180 // value _and_ its dynamic type in the plan, so we can recover 181 // exactly the same value later. 182 dv, err := plans.NewDynamicValue(iv.Value, cty.DynamicPseudoType) 183 if err != nil { 184 diags = diags.Append(tfdiags.Sourceless( 185 tfdiags.Error, 186 "Failed to prepare variable value for plan", 187 fmt.Sprintf("The value for variable %q could not be serialized to store in the plan: %s.", k, err), 188 )) 189 continue 190 } 191 varVals[k] = dv 192 } 193 194 // insert the run-specific data from the context into the plan; variables, 195 // targets and provider SHAs. 196 if plan != nil { 197 plan.VariableValues = varVals 198 plan.TargetAddrs = opts.Targets 199 } else if !diags.HasErrors() { 200 panic("nil plan but no errors") 201 } 202 203 relevantAttrs, rDiags := c.relevantResourceAttrsForPlan(config, plan) 204 diags = diags.Append(rDiags) 205 206 plan.RelevantAttributes = relevantAttrs 207 diags = diags.Append(c.checkApplyGraph(plan, config)) 208 209 return plan, diags 210 } 211 212 // checkApplyGraph builds the apply graph out of the current plan to 213 // check for any errors that may arise once the planned changes are added to 214 // the graph. This allows durgaform to report errors (mostly cycles) during 215 // plan that would otherwise only crop up during apply 216 func (c *Context) checkApplyGraph(plan *plans.Plan, config *configs.Config) tfdiags.Diagnostics { 217 if plan.Changes.Empty() { 218 log.Println("[DEBUG] no planned changes, skipping apply graph check") 219 return nil 220 } 221 log.Println("[DEBUG] building apply graph to check for errors") 222 _, _, diags := c.applyGraph(plan, config, true) 223 return diags 224 } 225 226 var DefaultPlanOpts = &PlanOpts{ 227 Mode: plans.NormalMode, 228 } 229 230 // SimplePlanOpts is a constructor to help with creating "simple" values of 231 // PlanOpts which only specify a mode and input variables. 232 // 233 // This helper function is primarily intended for use in straightforward 234 // tests that don't need any of the more "esoteric" planning options. For 235 // handling real user requests to run Durgaform, it'd probably be better 236 // to construct a *PlanOpts value directly and provide a way for the user 237 // to set values for all of its fields. 238 // 239 // The "mode" and "setVariables" arguments become the values of the "Mode" 240 // and "SetVariables" fields in the result. Refer to the PlanOpts type 241 // documentation to learn about the meanings of those fields. 242 func SimplePlanOpts(mode plans.Mode, setVariables InputValues) *PlanOpts { 243 return &PlanOpts{ 244 Mode: mode, 245 SetVariables: setVariables, 246 } 247 } 248 249 func (c *Context) plan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { 250 var diags tfdiags.Diagnostics 251 252 if opts.Mode != plans.NormalMode { 253 panic(fmt.Sprintf("called Context.plan with %s", opts.Mode)) 254 } 255 256 plan, walkDiags := c.planWalk(config, prevRunState, opts) 257 diags = diags.Append(walkDiags) 258 if diags.HasErrors() { 259 return nil, diags 260 } 261 262 // The refreshed state ends up with some placeholder objects in it for 263 // objects pending creation. We only really care about those being in 264 // the working state, since that's what we're going to use when applying, 265 // so we'll prune them all here. 266 plan.PriorState.SyncWrapper().RemovePlannedResourceInstanceObjects() 267 268 return plan, diags 269 } 270 271 func (c *Context) refreshOnlyPlan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { 272 var diags tfdiags.Diagnostics 273 274 if opts.Mode != plans.RefreshOnlyMode { 275 panic(fmt.Sprintf("called Context.refreshOnlyPlan with %s", opts.Mode)) 276 } 277 278 plan, walkDiags := c.planWalk(config, prevRunState, opts) 279 diags = diags.Append(walkDiags) 280 if diags.HasErrors() { 281 return nil, diags 282 } 283 284 // If the graph builder and graph nodes correctly obeyed our directive 285 // to refresh only, the set of resource changes should always be empty. 286 // We'll safety-check that here so we can return a clear message about it, 287 // rather than probably just generating confusing output at the UI layer. 288 if len(plan.Changes.Resources) != 0 { 289 // Some extra context in the logs in case the user reports this message 290 // as a bug, as a starting point for debugging. 291 for _, rc := range plan.Changes.Resources { 292 if depKey := rc.DeposedKey; depKey == states.NotDeposed { 293 log.Printf("[DEBUG] Refresh-only plan includes %s change for %s", rc.Action, rc.Addr) 294 } else { 295 log.Printf("[DEBUG] Refresh-only plan includes %s change for %s deposed object %s", rc.Action, rc.Addr, depKey) 296 } 297 } 298 diags = diags.Append(tfdiags.Sourceless( 299 tfdiags.Error, 300 "Invalid refresh-only plan", 301 "Durgaform generated planned resource changes in a refresh-only plan. This is a bug in Terraform.", 302 )) 303 } 304 305 // Prune out any placeholder objects we put in the state to represent 306 // objects that would need to be created. 307 plan.PriorState.SyncWrapper().RemovePlannedResourceInstanceObjects() 308 309 // We don't populate RelevantResources for a refresh-only plan, because 310 // they never have any planned actions and so no resource can ever be 311 // "relevant" per the intended meaning of that field. 312 313 return plan, diags 314 } 315 316 func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { 317 var diags tfdiags.Diagnostics 318 pendingPlan := &plans.Plan{} 319 320 if opts.Mode != plans.DestroyMode { 321 panic(fmt.Sprintf("called Context.destroyPlan with %s", opts.Mode)) 322 } 323 324 priorState := prevRunState 325 326 // A destroy plan starts by running Refresh to read any pending data 327 // sources, and remove missing managed resources. This is required because 328 // a "destroy plan" is only creating delete changes, and is essentially a 329 // local operation. 330 // 331 // NOTE: if skipRefresh _is_ set then we'll rely on the destroy-plan walk 332 // below to upgrade the prevRunState and priorState both to the latest 333 // resource type schemas, so NodePlanDestroyableResourceInstance.Execute 334 // must coordinate with this by taking that action only when c.skipRefresh 335 // _is_ set. This coupling between the two is unfortunate but necessary 336 // to work within our current structure. 337 if !opts.SkipRefresh { 338 log.Printf("[TRACE] Context.destroyPlan: calling Context.plan to get the effect of refreshing the prior state") 339 normalOpts := *opts 340 normalOpts.Mode = plans.NormalMode 341 refreshPlan, refreshDiags := c.plan(config, prevRunState, &normalOpts) 342 if refreshDiags.HasErrors() { 343 // NOTE: Normally we'd append diagnostics regardless of whether 344 // there are errors, just in case there are warnings we'd want to 345 // preserve, but we're intentionally _not_ doing that here because 346 // if the first plan succeeded then we'll be running another plan 347 // in DestroyMode below, and we don't want to double-up any 348 // warnings that both plan walks would generate. 349 // (This does mean we won't show any warnings that would've been 350 // unique to only this walk, but we're assuming here that if the 351 // warnings aren't also applicable to a destroy plan then we'd 352 // rather not show them here, because this non-destroy plan for 353 // refreshing is largely an implementation detail.) 354 diags = diags.Append(refreshDiags) 355 return nil, diags 356 } 357 358 // insert the refreshed state into the destroy plan result, and ignore 359 // the changes recorded from the refresh. 360 pendingPlan.PriorState = refreshPlan.PriorState.DeepCopy() 361 pendingPlan.PrevRunState = refreshPlan.PrevRunState.DeepCopy() 362 log.Printf("[TRACE] Context.destroyPlan: now _really_ creating a destroy plan") 363 364 // We'll use the refreshed state -- which is the "prior state" from 365 // the perspective of this "pending plan" -- as the starting state 366 // for our destroy-plan walk, so it can take into account if we 367 // detected during refreshing that anything was already deleted outside 368 // of Durgaform. 369 priorState = pendingPlan.PriorState 370 } 371 372 destroyPlan, walkDiags := c.planWalk(config, priorState, opts) 373 diags = diags.Append(walkDiags) 374 if walkDiags.HasErrors() { 375 return nil, diags 376 } 377 378 if !opts.SkipRefresh { 379 // If we didn't skip refreshing then we want the previous run state 380 // prior state to be the one we originally fed into the c.plan call 381 // above, not the refreshed version we used for the destroy walk. 382 destroyPlan.PrevRunState = pendingPlan.PrevRunState 383 } 384 385 relevantAttrs, rDiags := c.relevantResourceAttrsForPlan(config, destroyPlan) 386 diags = diags.Append(rDiags) 387 388 destroyPlan.RelevantAttributes = relevantAttrs 389 return destroyPlan, diags 390 } 391 392 func (c *Context) prePlanFindAndApplyMoves(config *configs.Config, prevRunState *states.State, targets []addrs.Targetable) ([]refactoring.MoveStatement, refactoring.MoveResults) { 393 explicitMoveStmts := refactoring.FindMoveStatements(config) 394 implicitMoveStmts := refactoring.ImpliedMoveStatements(config, prevRunState, explicitMoveStmts) 395 var moveStmts []refactoring.MoveStatement 396 if stmtsLen := len(explicitMoveStmts) + len(implicitMoveStmts); stmtsLen > 0 { 397 moveStmts = make([]refactoring.MoveStatement, 0, stmtsLen) 398 moveStmts = append(moveStmts, explicitMoveStmts...) 399 moveStmts = append(moveStmts, implicitMoveStmts...) 400 } 401 moveResults := refactoring.ApplyMoves(moveStmts, prevRunState) 402 return moveStmts, moveResults 403 } 404 405 func (c *Context) prePlanVerifyTargetedMoves(moveResults refactoring.MoveResults, targets []addrs.Targetable) tfdiags.Diagnostics { 406 if len(targets) < 1 { 407 return nil // the following only matters when targeting 408 } 409 410 var diags tfdiags.Diagnostics 411 412 var excluded []addrs.AbsResourceInstance 413 for _, result := range moveResults.Changes.Values() { 414 fromMatchesTarget := false 415 toMatchesTarget := false 416 for _, targetAddr := range targets { 417 if targetAddr.TargetContains(result.From) { 418 fromMatchesTarget = true 419 } 420 if targetAddr.TargetContains(result.To) { 421 toMatchesTarget = true 422 } 423 } 424 if !fromMatchesTarget { 425 excluded = append(excluded, result.From) 426 } 427 if !toMatchesTarget { 428 excluded = append(excluded, result.To) 429 } 430 } 431 if len(excluded) > 0 { 432 sort.Slice(excluded, func(i, j int) bool { 433 return excluded[i].Less(excluded[j]) 434 }) 435 436 var listBuf strings.Builder 437 var prevResourceAddr addrs.AbsResource 438 for _, instAddr := range excluded { 439 // Targeting generally ends up selecting whole resources rather 440 // than individual instances, because we don't factor in 441 // individual instances until DynamicExpand, so we're going to 442 // always show whole resource addresses here, excluding any 443 // instance keys. (This also neatly avoids dealing with the 444 // different quoting styles required for string instance keys 445 // on different shells, which is handy.) 446 // 447 // To avoid showing duplicates when we have multiple instances 448 // of the same resource, we'll remember the most recent 449 // resource we rendered in prevResource, which is sufficient 450 // because we sorted the list of instance addresses above, and 451 // our sort order always groups together instances of the same 452 // resource. 453 resourceAddr := instAddr.ContainingResource() 454 if resourceAddr.Equal(prevResourceAddr) { 455 continue 456 } 457 fmt.Fprintf(&listBuf, "\n -target=%q", resourceAddr.String()) 458 prevResourceAddr = resourceAddr 459 } 460 diags = diags.Append(tfdiags.Sourceless( 461 tfdiags.Error, 462 "Moved resource instances excluded by targeting", 463 fmt.Sprintf( 464 "Resource instances in your current state have moved to new addresses in the latest configuration. Durgaform must include those resource instances while planning in order to ensure a correct result, but your -target=... options to not fully cover all of those resource instances.\n\nTo create a valid plan, either remove your -target=... options altogether or add the following additional target options:%s\n\nNote that adding these options may include further additional resource instances in your plan, in order to respect object dependencies.", 465 listBuf.String(), 466 ), 467 )) 468 } 469 470 return diags 471 } 472 473 func (c *Context) postPlanValidateMoves(config *configs.Config, stmts []refactoring.MoveStatement, allInsts instances.Set) tfdiags.Diagnostics { 474 return refactoring.ValidateMoves(stmts, config, allInsts) 475 } 476 477 func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { 478 var diags tfdiags.Diagnostics 479 log.Printf("[DEBUG] Building and walking plan graph for %s", opts.Mode) 480 481 prevRunState = prevRunState.DeepCopy() // don't modify the caller's object when we process the moves 482 moveStmts, moveResults := c.prePlanFindAndApplyMoves(config, prevRunState, opts.Targets) 483 484 // If resource targeting is in effect then it might conflict with the 485 // move result. 486 diags = diags.Append(c.prePlanVerifyTargetedMoves(moveResults, opts.Targets)) 487 if diags.HasErrors() { 488 // We'll return early here, because if we have any moved resource 489 // instances excluded by targeting then planning is likely to encounter 490 // strange problems that may lead to confusing error messages. 491 return nil, diags 492 } 493 494 graph, walkOp, moreDiags := c.planGraph(config, prevRunState, opts) 495 diags = diags.Append(moreDiags) 496 if diags.HasErrors() { 497 return nil, diags 498 } 499 500 // If we get here then we should definitely have a non-nil "graph", which 501 // we can now walk. 502 changes := plans.NewChanges() 503 conditions := plans.NewConditions() 504 walker, walkDiags := c.walk(graph, walkOp, &graphWalkOpts{ 505 Config: config, 506 InputState: prevRunState, 507 Changes: changes, 508 Conditions: conditions, 509 MoveResults: moveResults, 510 }) 511 diags = diags.Append(walker.NonFatalDiagnostics) 512 diags = diags.Append(walkDiags) 513 moveValidateDiags := c.postPlanValidateMoves(config, moveStmts, walker.InstanceExpander.AllInstances()) 514 if moveValidateDiags.HasErrors() { 515 // If any of the move statements are invalid then those errors take 516 // precedence over any other errors because an incomplete move graph 517 // is quite likely to be the _cause_ of various errors. This oddity 518 // comes from the fact that we need to apply the moves before we 519 // actually validate them, because validation depends on the result 520 // of first trying to plan. 521 return nil, moveValidateDiags 522 } 523 diags = diags.Append(moveValidateDiags) // might just contain warnings 524 525 if moveResults.Blocked.Len() > 0 && !diags.HasErrors() { 526 // If we had blocked moves and we're not going to be returning errors 527 // then we'll report the blockers as a warning. We do this only in the 528 // absense of errors because invalid move statements might well be 529 // the root cause of the blockers, and so better to give an actionable 530 // error message than a less-actionable warning. 531 diags = diags.Append(blockedMovesWarningDiag(moveResults)) 532 } 533 534 prevRunState = walker.PrevRunState.Close() 535 priorState := walker.RefreshState.Close() 536 driftedResources, driftDiags := c.driftedResources(config, prevRunState, priorState, moveResults) 537 diags = diags.Append(driftDiags) 538 539 plan := &plans.Plan{ 540 UIMode: opts.Mode, 541 Changes: changes, 542 Conditions: conditions, 543 DriftedResources: driftedResources, 544 PrevRunState: prevRunState, 545 PriorState: priorState, 546 547 // Other fields get populated by Context.Plan after we return 548 } 549 return plan, diags 550 } 551 552 func (c *Context) planGraph(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*Graph, walkOperation, tfdiags.Diagnostics) { 553 switch mode := opts.Mode; mode { 554 case plans.NormalMode: 555 graph, diags := (&PlanGraphBuilder{ 556 Config: config, 557 State: prevRunState, 558 RootVariableValues: opts.SetVariables, 559 Plugins: c.plugins, 560 Targets: opts.Targets, 561 ForceReplace: opts.ForceReplace, 562 skipRefresh: opts.SkipRefresh, 563 Operation: walkPlan, 564 }).Build(addrs.RootModuleInstance) 565 return graph, walkPlan, diags 566 case plans.RefreshOnlyMode: 567 graph, diags := (&PlanGraphBuilder{ 568 Config: config, 569 State: prevRunState, 570 RootVariableValues: opts.SetVariables, 571 Plugins: c.plugins, 572 Targets: opts.Targets, 573 skipRefresh: opts.SkipRefresh, 574 skipPlanChanges: true, // this activates "refresh only" mode. 575 Operation: walkPlan, 576 }).Build(addrs.RootModuleInstance) 577 return graph, walkPlan, diags 578 case plans.DestroyMode: 579 graph, diags := (&PlanGraphBuilder{ 580 Config: config, 581 State: prevRunState, 582 RootVariableValues: opts.SetVariables, 583 Plugins: c.plugins, 584 Targets: opts.Targets, 585 skipRefresh: opts.SkipRefresh, 586 Operation: walkPlanDestroy, 587 }).Build(addrs.RootModuleInstance) 588 return graph, walkPlanDestroy, diags 589 default: 590 // The above should cover all plans.Mode values 591 panic(fmt.Sprintf("unsupported plan mode %s", mode)) 592 } 593 } 594 595 func (c *Context) driftedResources(config *configs.Config, oldState, newState *states.State, moves refactoring.MoveResults) ([]*plans.ResourceInstanceChangeSrc, tfdiags.Diagnostics) { 596 var diags tfdiags.Diagnostics 597 598 if newState.ManagedResourcesEqual(oldState) && moves.Changes.Len() == 0 { 599 // Nothing to do, because we only detect and report drift for managed 600 // resource instances. 601 return nil, diags 602 } 603 604 schemas, schemaDiags := c.Schemas(config, newState) 605 diags = diags.Append(schemaDiags) 606 if diags.HasErrors() { 607 return nil, diags 608 } 609 610 var drs []*plans.ResourceInstanceChangeSrc 611 612 for _, ms := range oldState.Modules { 613 for _, rs := range ms.Resources { 614 if rs.Addr.Resource.Mode != addrs.ManagedResourceMode { 615 // Drift reporting is only for managed resources 616 continue 617 } 618 619 provider := rs.ProviderConfig.Provider 620 for key, oldIS := range rs.Instances { 621 if oldIS.Current == nil { 622 // Not interested in instances that only have deposed objects 623 continue 624 } 625 addr := rs.Addr.Instance(key) 626 627 // Previous run address defaults to the current address, but 628 // can differ if the resource moved before refreshing 629 prevRunAddr := addr 630 if move, ok := moves.Changes.GetOk(addr); ok { 631 prevRunAddr = move.From 632 } 633 634 newIS := newState.ResourceInstance(addr) 635 636 schema, _ := schemas.ResourceTypeConfig( 637 provider, 638 addr.Resource.Resource.Mode, 639 addr.Resource.Resource.Type, 640 ) 641 if schema == nil { 642 // This should never happen, but just in case 643 return nil, diags.Append(tfdiags.Sourceless( 644 tfdiags.Error, 645 "Missing resource schema from provider", 646 fmt.Sprintf("No resource schema found for %s.", addr.Resource.Resource.Type), 647 )) 648 } 649 ty := schema.ImpliedType() 650 651 oldObj, err := oldIS.Current.Decode(ty) 652 if err != nil { 653 // This should also never happen 654 return nil, diags.Append(tfdiags.Sourceless( 655 tfdiags.Error, 656 "Failed to decode resource from state", 657 fmt.Sprintf("Error decoding %q from previous state: %s", addr.String(), err), 658 )) 659 } 660 661 var newObj *states.ResourceInstanceObject 662 if newIS != nil && newIS.Current != nil { 663 newObj, err = newIS.Current.Decode(ty) 664 if err != nil { 665 // This should also never happen 666 return nil, diags.Append(tfdiags.Sourceless( 667 tfdiags.Error, 668 "Failed to decode resource from state", 669 fmt.Sprintf("Error decoding %q from prior state: %s", addr.String(), err), 670 )) 671 } 672 } 673 674 var oldVal, newVal cty.Value 675 oldVal = oldObj.Value 676 if newObj != nil { 677 newVal = newObj.Value 678 } else { 679 newVal = cty.NullVal(ty) 680 } 681 682 if oldVal.RawEquals(newVal) && addr.Equal(prevRunAddr) { 683 // No drift if the two values are semantically equivalent 684 // and no move has happened 685 continue 686 } 687 688 // We can detect three types of changes after refreshing state, 689 // only two of which are easily understood as "drift": 690 // 691 // - Resources which were deleted outside of Durgaform; 692 // - Resources where the object value has changed outside of 693 // Durgaform; 694 // - Resources which have been moved without other changes. 695 // 696 // All of these are returned as drift, to allow refresh-only plans 697 // to present a full set of changes which will be applied. 698 var action plans.Action 699 switch { 700 case newVal.IsNull(): 701 action = plans.Delete 702 case !oldVal.RawEquals(newVal): 703 action = plans.Update 704 default: 705 action = plans.NoOp 706 } 707 708 change := &plans.ResourceInstanceChange{ 709 Addr: addr, 710 PrevRunAddr: prevRunAddr, 711 ProviderAddr: rs.ProviderConfig, 712 Change: plans.Change{ 713 Action: action, 714 Before: oldVal, 715 After: newVal, 716 }, 717 } 718 719 changeSrc, err := change.Encode(ty) 720 if err != nil { 721 diags = diags.Append(err) 722 return nil, diags 723 } 724 725 drs = append(drs, changeSrc) 726 } 727 } 728 } 729 730 return drs, diags 731 } 732 733 // PlanGraphForUI is a last vestage of graphs in the public interface of Context 734 // (as opposed to graphs as an implementation detail) intended only for use 735 // by the "durgaform graph" command when asked to render a plan-time graph. 736 // 737 // The result of this is intended only for rendering ot the user as a dot 738 // graph, and so may change in future in order to make the result more useful 739 // in that context, even if drifts away from the physical graph that Durgaform 740 // Core currently uses as an implementation detail of planning. 741 func (c *Context) PlanGraphForUI(config *configs.Config, prevRunState *states.State, mode plans.Mode) (*Graph, tfdiags.Diagnostics) { 742 // For now though, this really is just the internal graph, confusing 743 // implementation details and all. 744 745 var diags tfdiags.Diagnostics 746 747 opts := &PlanOpts{Mode: mode} 748 749 graph, _, moreDiags := c.planGraph(config, prevRunState, opts) 750 diags = diags.Append(moreDiags) 751 return graph, diags 752 } 753 754 func blockedMovesWarningDiag(results refactoring.MoveResults) tfdiags.Diagnostic { 755 if results.Blocked.Len() < 1 { 756 // Caller should check first 757 panic("request to render blocked moves warning without any blocked moves") 758 } 759 760 var itemsBuf bytes.Buffer 761 for _, blocked := range results.Blocked.Values() { 762 fmt.Fprintf(&itemsBuf, "\n - %s could not move to %s", blocked.Actual, blocked.Wanted) 763 } 764 765 return tfdiags.Sourceless( 766 tfdiags.Warning, 767 "Unresolved resource instance address changes", 768 fmt.Sprintf( 769 "Durgaform tried to adjust resource instance addresses in the prior state based on change information recorded in the configuration, but some adjustments did not succeed due to existing objects already at the intended addresses:%s\n\nTerraform has planned to destroy these objects. If Terraform's proposed changes aren't appropriate, you must first resolve the conflicts using the \"durgaform state\" subcommands and then create a new plan.", 770 itemsBuf.String(), 771 ), 772 ) 773 } 774 775 // referenceAnalyzer returns a globalref.Analyzer object to help with 776 // global analysis of references within the configuration that's attached 777 // to the receiving context. 778 func (c *Context) referenceAnalyzer(config *configs.Config, state *states.State) (*globalref.Analyzer, tfdiags.Diagnostics) { 779 schemas, diags := c.Schemas(config, state) 780 if diags.HasErrors() { 781 return nil, diags 782 } 783 return globalref.NewAnalyzer(config, schemas.Providers), diags 784 } 785 786 // relevantResourcesForPlan implements the heuristic we use to populate the 787 // RelevantResources field of returned plans. 788 func (c *Context) relevantResourceAttrsForPlan(config *configs.Config, plan *plans.Plan) ([]globalref.ResourceAttr, tfdiags.Diagnostics) { 789 azr, diags := c.referenceAnalyzer(config, plan.PriorState) 790 if diags.HasErrors() { 791 return nil, diags 792 } 793 794 var refs []globalref.Reference 795 for _, change := range plan.Changes.Resources { 796 if change.Action == plans.NoOp { 797 continue 798 } 799 800 moreRefs := azr.ReferencesFromResourceInstance(change.Addr) 801 refs = append(refs, moreRefs...) 802 } 803 804 for _, change := range plan.Changes.Outputs { 805 if change.Action == plans.NoOp { 806 continue 807 } 808 809 moreRefs := azr.ReferencesFromOutputValue(change.Addr) 810 refs = append(refs, moreRefs...) 811 } 812 813 var contributors []globalref.ResourceAttr 814 815 for _, ref := range azr.ContributingResourceReferences(refs...) { 816 if res, ok := ref.ResourceAttr(); ok { 817 contributors = append(contributors, res) 818 } 819 } 820 821 return contributors, diags 822 }