Files
opentf/internal/engine/planning/plan.go
Martin Atkins b6cdfd1afa lang/eval: Bind PlanningOracle to PlanGlue
Previously the PlanGlue methods all took PlanningOracle pointers as one
of their arguments, which is annoying since all of them should end up with
pointers to the same object and it makes it hard for the PlanGlue
implementation to do any work outside of and between the PlanGlue method
calls.

Instead then we'll have DrivePlanning take a function for building a
PlanGlue implementation given a PlanningOracle pointer, and then the
planning engine returns an implementation that binds a planContext to a
PlanningOracle it can then use to do all of its work.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-10-27 10:15:41 -07:00

108 lines
4.8 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package planning
import (
"context"
"github.com/opentofu/opentofu/internal/lang/eval"
"github.com/opentofu/opentofu/internal/lang/grapheval"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/states"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// PlanChanges is the main entry point, taking a state snapshot from the end
// of the previous plan/apply round and an instantiated configuration (bound
// to some input variable definitions) and returning a plan containing a set of
// proposed actions.
//
// This is currently really just a placeholder to demonstrate the role that the
// functionality in lang/eval might play in a planning process and what other
// work would need to happen in a planning engine that is beyond the scope
// of lang/eval. For now then it's just using our existing models of state
// and plan, but our larger ambitions also involve some other changes that
// would likely cause the signature here to change significantly:
//
// - We're considering changing the apply phase implementation to just be a
// walk of an execution graph calculated during the planning phase, which
// implies that the "plan" model would need to change significantly to
// be able to directly represent that graph, whereas the current model only
// _implies_ that graph at a high level while expecting the apply phase
// itself to construct the finalized graph.
// - We're considering switching from a "state snapshot" model to a more
// granular model where we request individual objects from the state storage
// as needed, in which case we'd likely change our usage pattern so that
// the planning phase is able to create a "provider-like" live object that
// offers an API for fetching items from the state as needed, rather than
// the current pure-data snapshot representation.
//
// Therefore readers of this code should focus mainly on the inner
// implementation of how it decides what to plan and how to plan it, and less
// on where it gets the information to make those decisions and how it
// represents those decisions in its return value.
func PlanChanges(ctx context.Context, prevRoundState *states.State, configInst *eval.ConfigInstance) (*plans.Plan, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
planCtx := newPlanContext(configInst.EvalContext(), prevRoundState)
// This configInst.DrivePlanning call blocks until the evaluator has
// visited all expressions in the configuration and calls
// [planContext.PlanDesiredResourceInstance] on the [planGlue] object for
// each resource instance it discovers so that we can produce a planned
// action and result value for each one.
//
// It also calls the various "Plan*Orphans" methods at different levels
// of granularity once it's determined the full set of objects under
// a given prefix, which planGlue uses to notice when there are
// prevRoundState resource instances that are no longer in the desired
// state and so plan to delete or forget them.
evalResult, moreDiags := configInst.DrivePlanning(ctx, func(oracle *eval.PlanningOracle) eval.PlanGlue {
return &planGlue{
planCtx: planCtx,
oracle: oracle,
}
})
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
// If we encountered errors during the eval-based phase then we'll halt
// here but we'll still produce a best-effort [plans.Plan] describing
// the situation because that often gives useful information for debugging
// what caused the errors.
plan := planCtx.Close()
plan.Errored = true
return plan, diags
}
// We also need to deal with any "deposed" resource instances that were
// in the previous round state. We do this separately afterwards because
// these have no direct representation in the configuration at all and
// so are not in scope for the config eval system. It's also relatively
// rare for a previous round state to include deposed instances, since it
// can happen only if the "delete" leg of a create-before-destroy replace
// failed in the previous round.
//
// The provider instance manager should've planned ahead and arranged for
// any providers we need for these to still be open, waiting for the
// completion reports generated by our planning calls in this loop.
ctx = grapheval.ContextWithNewWorker(ctx)
planGlue := evalResult.Glue.(*planGlue)
for _, moduleState := range prevRoundState.Modules {
for _, resourceState := range moduleState.Resources {
for instKey, instState := range resourceState.Instances {
instAddr := resourceState.Addr.Instance(instKey)
for dk := range instState.Deposed {
diags = diags.Append(
planGlue.planDeposedResourceInstanceObject(ctx, instAddr, dk, instState),
)
}
}
}
}
return planCtx.Close(), diags
}