opentofu/internal/terraform/context_walk.go
Martin Atkins d63871f70d core: Propagate check results accurately from plan to apply
In an earlier commit we changed the states.CheckResults model to
explicitly model the config object vs. dynamic checkable object hierarchy,
but neglected to update the logic in Terraform Core to take that into
account when propagating the object expansion decisions from the plan
phase to the apply phase. That meant that we were incorrectly classifying
zero-instance resources always as having an unknown number of instances,
rather than possibly being known to have zero instances.

This now follows the two-level heirarchy of the data structure, which has
the nice side-effect that we can remove some of the special-case methods
from checks.State that we were using to bulk-load data: the data is now
shaped in the appropriate way to reload the data using the same method
the plan phase would've used to record the results in the first place.
2022-08-26 15:47:29 -07:00

145 lines
4.9 KiB
Go

package terraform
import (
"log"
"github.com/hashicorp/terraform/internal/checks"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/refactoring"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// graphWalkOpts captures some transient values we use (and possibly mutate)
// during a graph walk.
//
// The way these options get used unfortunately varies between the different
// walkOperation types. This is a historical design wart that dates back to
// us using the same graph structure for all operations; hopefully we'll
// make the necessary differences between the walk types more explicit someday.
type graphWalkOpts struct {
InputState *states.State
Changes *plans.Changes
Config *configs.Config
// PlanTimeCheckResults should be populated during the apply phase with
// the snapshot of check results that was generated during the plan step.
//
// This then propagates the decisions about which checkable objects exist
// from the plan phase into the apply phase without having to re-compute
// the module and resource expansion.
PlanTimeCheckResults *states.CheckResults
MoveResults refactoring.MoveResults
}
func (c *Context) walk(graph *Graph, operation walkOperation, opts *graphWalkOpts) (*ContextGraphWalker, tfdiags.Diagnostics) {
log.Printf("[DEBUG] Starting graph walk: %s", operation.String())
walker := c.graphWalker(operation, opts)
// Watch for a stop so we can call the provider Stop() API.
watchStop, watchWait := c.watchStop(walker)
// Walk the real graph, this will block until it completes
diags := graph.Walk(walker)
// Close the channel so the watcher stops, and wait for it to return.
close(watchStop)
<-watchWait
return walker, diags
}
func (c *Context) graphWalker(operation walkOperation, opts *graphWalkOpts) *ContextGraphWalker {
var state *states.SyncState
var refreshState *states.SyncState
var prevRunState *states.SyncState
// NOTE: None of the SyncState objects must directly wrap opts.InputState,
// because we use those to mutate the state object and opts.InputState
// belongs to our caller and thus we must treat it as immutable.
//
// To account for that, most of our SyncState values created below end up
// wrapping a _deep copy_ of opts.InputState instead.
inputState := opts.InputState
if inputState == nil {
// Lots of callers use nil to represent the "empty" case where we've
// not run Apply yet, so we tolerate that.
inputState = states.NewState()
}
switch operation {
case walkValidate:
// validate should not use any state
state = states.NewState().SyncWrapper()
// validate currently uses the plan graph, so we have to populate the
// refreshState and the prevRunState.
refreshState = states.NewState().SyncWrapper()
prevRunState = states.NewState().SyncWrapper()
case walkPlan, walkPlanDestroy, walkImport:
state = inputState.DeepCopy().SyncWrapper()
refreshState = inputState.DeepCopy().SyncWrapper()
prevRunState = inputState.DeepCopy().SyncWrapper()
// For both of our new states we'll discard the previous run's
// check results, since we can still refer to them from the
// prevRunState object if we need to.
state.DiscardCheckResults()
refreshState.DiscardCheckResults()
default:
state = inputState.DeepCopy().SyncWrapper()
// Only plan-like walks use refreshState and prevRunState
// Discard the input state's check results, because we should create
// a new set as a result of the graph walk.
state.DiscardCheckResults()
}
changes := opts.Changes
if changes == nil {
// Several of our non-plan walks end up sharing codepaths with the
// plan walk and thus expect to generate planned changes even though
// we don't care about them. To avoid those crashing, we'll just
// insert a placeholder changes object which'll get discarded
// afterwards.
changes = plans.NewChanges()
}
if opts.Config == nil {
panic("Context.graphWalker call without Config")
}
checkState := checks.NewState(opts.Config)
if opts.PlanTimeCheckResults != nil {
// We'll re-report all of the same objects we determined during the
// plan phase so that we can repeat the checks during the apply
// phase to finalize them.
for _, configElem := range opts.PlanTimeCheckResults.ConfigResults.Elems {
if configElem.Value.ObjectAddrsKnown() {
configAddr := configElem.Key
checkState.ReportCheckableObjects(configAddr, configElem.Value.ObjectResults.Keys())
}
}
}
return &ContextGraphWalker{
Context: c,
State: state,
Config: opts.Config,
RefreshState: refreshState,
PrevRunState: prevRunState,
Changes: changes.SyncWrapper(),
Checks: checkState,
InstanceExpander: instances.NewExpander(),
MoveResults: opts.MoveResults,
Operation: operation,
StopContext: c.runContext,
}
}