Files
opentf/internal/engine/planning/plan_context.go
2025-12-01 13:09:58 -05:00

98 lines
3.5 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 (
"log"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/engine/lifecycle"
"github.com/opentofu/opentofu/internal/engine/plugins"
"github.com/opentofu/opentofu/internal/lang/eval"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/states"
)
// planContext is our shared state for the various parts of a single call
// to [PlanChanges], and is mainly used as part of our [eval.PlanGlue]
// implementation [planGlue], through which the evaluator calls us to ask for
// planning results.
type planContext struct {
evalCtx *eval.EvalContext
plannedChanges *plans.ChangesSync
// TODO: The following should probably track a reason why each resource
// instance was deferred, but since deferral is not the focus of this
// current experiment we'll just keep this boolean for now.
deferred addrs.Map[addrs.AbsResourceInstance, struct{}]
// prevRoundState MUST be treated as immutable
prevRoundState *states.State
// refreshedState is where we record the results of refreshing
// resource instances as we visit them. This starts as a deep copy
// of prevRoundState.
refreshedState *states.SyncState
completion *completionTracker
providerInstances *providerInstances
providers plugins.Providers
// TODO: something to track which ephemeral resource instances are currently
// open? (Do we actually need that, or can we just rely on a background
// goroutine to babysit those based on the completion tracker?)
}
func newPlanContext(evalCtx *eval.EvalContext, prevRoundState *states.State, providers plugins.Providers) *planContext {
if prevRoundState == nil {
prevRoundState = states.NewState()
}
changes := plans.NewChanges()
refreshedState := prevRoundState.DeepCopy()
completion := lifecycle.NewCompletionTracker[completionEvent]()
return &planContext{
evalCtx: evalCtx,
plannedChanges: changes.SyncWrapper(),
prevRoundState: prevRoundState,
refreshedState: refreshedState.SyncWrapper(),
completion: completion,
providerInstances: newProviderInstances(completion),
providers: providers,
}
}
// Close marks the end of the use of the [planContext] object, returning a
// [plans.Plan] representation of the plan that was created.
//
// After calling this function the [planContext] object is invalid and must
// not be used anymore.
func (p *planContext) Close() *plans.Plan {
// Before we return we'll make sure our completion tracker isn't waiting
// for anything else to complete, so that we can unblock closing of
// any provider instances or ephemeral resource instances that might've
// got left behind by panics/etc. We should not be relying on this in the
// happy path.
for event := range p.completion.PendingItems() {
log.Printf("[TRACE] planContext: synthetic completion of %#v", event)
p.completion.ReportCompletion(event)
}
return &plans.Plan{
UIMode: plans.NormalMode, // TODO: This PlanChanges function needs something analogous to [tofu.PlanOpts] for planning mode/options
Changes: p.plannedChanges.Close(),
PrevRunState: p.prevRoundState,
PriorState: p.refreshedState.Close(),
// TODO: various other fields that we need to actually make use
// of this plan result. But this is intentionally just a partial
// result for now because it's not clear that we'd even be using
// plans.Plan in a final version of this new approach.
}
}