core: Possibly return a partial plan alongside errors

For some kinds of plan failure we will already have successfully completed
planning for at least one upstream object before encountering a downstream
error.

Since a downstream failure can be caused by an already-recorded action
from upstream, it might be helpful to inspect the actions planned so far
in order to understand better why the error occurred.

This doesn't yet make this result visible anywhere, and is backward
compatible with existing callers because they currently entirely ignore
the returned plan pointer if the diagnostics contains at least one error.
This commit is contained in:
Martin Atkins 2022-05-16 10:41:50 -07:00 committed by James Bardin
parent a9230c9e75
commit 6611e3a9cd

View File

@ -71,14 +71,21 @@ type PlanOpts struct {
ForceReplace []addrs.AbsResourceInstance
}
// Plan generates an execution plan for the given context, and returns the
// refreshed state.
// Plan generates an execution plan by comparing the given configuration
// with the given previous run state.
//
// The execution plan encapsulates the context and can be stored
// in order to reinstantiate a context later for Apply.
// The given planning options allow control of various other details of the
// planning process that are not represented directly in the configuration.
// You can use terraform.DefaultPlanOpts to generate a normal plan with no
// special options.
//
// Plan also updates the diff of this context to be the diff generated
// by the plan, so Apply can be called after.
// If the returned diagnostics contains no errors then the returned plan is
// applyable, although Terraform cannot guarantee that applying it will fully
// succeed. If the returned diagnostics contains errors but this method
// still returns a non-nil Plan then the plan describes the subset of actions
// planned so far, which is not safe to apply but could potentially be used
// by the UI layer to give extra context to support understanding of the
// returned error messages.
func (c *Context) Plan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) {
defer c.acquireRun("plan")()
var diags tfdiags.Diagnostics
@ -175,9 +182,10 @@ The -target option is not for routine use, and is provided only for exceptional
panic(fmt.Sprintf("unsupported plan mode %s", opts.Mode))
}
diags = diags.Append(planDiags)
if diags.HasErrors() {
return nil, diags
}
// NOTE: We're intentionally not returning early when diags.HasErrors
// here because we'll still populate other metadata below on a best-effort
// basis to try to give the UI some extra context to return alongside the
// error messages.
// convert the variables into the format expected for the plan
varVals := make(map[string]plans.DynamicValue, len(opts.SetVariables))
@ -210,10 +218,18 @@ The -target option is not for routine use, and is provided only for exceptional
panic("nil plan but no errors")
}
relevantAttrs, rDiags := c.relevantResourceAttrsForPlan(config, plan)
diags = diags.Append(rDiags)
if plan != nil {
relevantAttrs, rDiags := c.relevantResourceAttrsForPlan(config, plan)
diags = diags.Append(rDiags)
plan.RelevantAttributes = relevantAttrs
}
if diags.HasErrors() {
// We can't proceed further with an invalid plan, because an invalid
// plan isn't applyable by definition.
return plan, diags
}
plan.RelevantAttributes = relevantAttrs
diags = diags.Append(c.checkApplyGraph(plan, config))
return plan, diags
@ -266,7 +282,10 @@ func (c *Context) plan(config *configs.Config, prevRunState *states.State, opts
plan, walkDiags := c.planWalk(config, prevRunState, opts)
diags = diags.Append(walkDiags)
if diags.HasErrors() {
return nil, diags
// Non-nil plan along with errors indicates a non-applyable partial
// plan that's only suitable to be shown to the user as extra context
// to help understand the errors.
return plan, diags
}
// The refreshed state ends up with some placeholder objects in it for
@ -288,7 +307,10 @@ func (c *Context) refreshOnlyPlan(config *configs.Config, prevRunState *states.S
plan, walkDiags := c.planWalk(config, prevRunState, opts)
diags = diags.Append(walkDiags)
if diags.HasErrors() {
return nil, diags
// Non-nil plan along with errors indicates a non-applyable partial
// plan that's only suitable to be shown to the user as extra context
// to help understand the errors.
return plan, diags
}
// If the graph builder and graph nodes correctly obeyed our directive
@ -388,7 +410,10 @@ func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State
destroyPlan, walkDiags := c.planWalk(config, priorState, opts)
diags = diags.Append(walkDiags)
if walkDiags.HasErrors() {
return nil, diags
// Non-nil plan along with errors indicates a non-applyable partial
// plan that's only suitable to be shown to the user as extra context
// to help understand the errors.
return destroyPlan, diags
}
if !opts.SkipRefresh {
@ -545,6 +570,12 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o
diags = diags.Append(blockedMovesWarningDiag(moveResults))
}
// If we reach this point with error diagnostics then "changes" is a
// representation of the subset of changes we were able to plan before
// we encountered errors, which we'll return as part of a non-nil plan
// so that e.g. the UI can show what was planned so far in case that extra
// context helps the user to understand the error messages we're returning.
prevRunState = walker.PrevRunState.Close()
priorState := walker.RefreshState.Close()
driftedResources, driftDiags := c.driftedResources(config, prevRunState, priorState, moveResults)