Files
opentf/internal/engine/planning/execgraph_managed.go
Martin Atkins 6981b9f8c6 planning: Dependencies between resource instance object subgraphs
The previous commit arranged for each resource instance object with a
planned change to have an execution subgraph generated for it, but didn't
honor the dependencies between those objects.

There's now a followup loop that adds all of the needed "waiter" edges
after the fact, including both the "forward" dependencies between
create/update changes and the "reverse" dependencies between delete
changes.

The shape of the leaf code here got quite messy. In future commits I intend
to work on cleaning up the details more, but the main focus here was to
restore the execgraph building functionality just enough to prove that this
new two-pass planning approach gives us enough information to insert
all of the needed dependency edges.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2026-02-23 10:25:42 -08:00

342 lines
15 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 (
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/engine/internal/exec"
"github.com/opentofu/opentofu/internal/engine/internal/execgraph"
"github.com/opentofu/opentofu/internal/lang/eval"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/states"
)
////////////////////////////////////////////////////////////////////////////////
// This file contains methods of [execGraphBuilder] that are related to the
// parts of an execution graph that deal with resource instances of mode
// [addrs.ManagedResourceMode] in particular.
////////////////////////////////////////////////////////////////////////////////
// ManagedResourceSubgraph adds graph nodes needed to apply changes for a
// managed resource instance, and returns various items needed to describe
// its relationships with other resource instance and provider instance
// subgraphs.
func (b *execGraphBuilder) ManagedResourceInstanceSubgraph(
plannedChange *plans.ResourceInstanceChange,
effectiveReplaceOrder resourceInstanceReplaceOrder,
providerClientRef execgraph.ResultRef[*exec.ProviderClient],
) (
valueRef, deletionRef execgraph.ResourceInstanceResultRef, // reference to the final new value and, if addDeleteDep is not nil, the deletion result
addConfigDep, addDeleteDep func(execgraph.AnyResultRef), // callbacks to register explicit dependencies, or nil when not relevant
) {
b.mu.Lock()
defer b.mu.Unlock()
// Before we go any further we'll just make sure what we've been given
// is sensible, so that the remaining code can assume the following
// about the given change. Any panics in the following suggest that there's
// a bug in the caller, unless we're intentionally changing the rules
// for what the different action types represent.
if plannedChange.DeposedKey != states.NotDeposed && plannedChange.Action != plans.Delete {
// The only sensible thing to do with a deposed object is to delete it.
panic(fmt.Sprintf("invalid action %s for %s deposed object %s", plannedChange.Action, plannedChange.PrevRunAddr, plannedChange.DeposedKey))
}
if plannedChange.Action == plans.Create && !plannedChange.Before.IsNull() {
panic(fmt.Sprintf("for %s has action %s but non-null prior value", plannedChange.Addr, plannedChange.Action))
}
if (plannedChange.Action == plans.Delete || plannedChange.Action == plans.Forget) && !plannedChange.After.IsNull() {
panic(fmt.Sprintf("change for %s has action %s but non-null planned new value", plannedChange.PrevRunAddr, plannedChange.Action))
}
if plannedChange.Action != plans.Create && plannedChange.Action != plans.Delete && plannedChange.Action != plans.Forget && (plannedChange.Before.IsNull() || plannedChange.After.IsNull()) {
panic(fmt.Sprintf("change for %s has action %s but does not have both a before and after value", plannedChange.PrevRunAddr, plannedChange.Action))
}
changeAction := plannedChange.Action
if changeAction.IsReplace() {
// The effective replace order finalizes which of the two replace
// actions we will actually use.
changeAction = effectiveReplaceOrder.ChangeAction()
}
// The shape of execution subgraph we generate here varies depending on
// which change action was planned.
switch changeAction {
case plans.Create:
valueRef, addConfigDep = b.managedResourceInstanceSubgraphCreate(plannedChange, providerClientRef)
case plans.Update:
valueRef, addConfigDep = b.managedResourceInstanceSubgraphUpdate(plannedChange, providerClientRef)
case plans.Delete:
deletionRef, addDeleteDep = b.managedResourceInstanceSubgraphDelete(plannedChange, providerClientRef)
case plans.Forget:
valueRef = b.managedResourceInstanceSubgraphForget(plannedChange, providerClientRef)
case plans.DeleteThenCreate, plans.ForgetThenCreate:
valueRef, deletionRef, addConfigDep, addDeleteDep = b.managedResourceInstanceSubgraphDeleteOrForgetThenCreate(plannedChange, providerClientRef)
case plans.CreateThenDelete:
valueRef, deletionRef, addConfigDep, addDeleteDep = b.managedResourceInstanceSubgraphCreateThenDelete(plannedChange, providerClientRef)
default:
// FIXME: We need to handle plans.NoOp too because that can occur if
// the configuration hasn't changed but the object will move to a
// new resource instance address during the apply phase.
// We should not get here: the cases above should cover every action
// that [planGlue.planDesiredManagedResourceInstance] can possibly
// produce.
panic(fmt.Sprintf("unsupported change action %s for %s", plannedChange.Action, plannedChange.Addr))
}
return valueRef, deletionRef, addConfigDep, addDeleteDep
}
func (b *execGraphBuilder) managedResourceInstanceSubgraphCreate(
plannedChange *plans.ResourceInstanceChange,
providerClientRef execgraph.ResultRef[*exec.ProviderClient],
) (execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef)) {
instAddrRef, _ := b.managedResourceInstanceChangeAddrAndPriorStateRefs(plannedChange)
plannedValRef := b.lower.ConstantValue(plannedChange.After)
waitFor, addConfigDep := b.lower.MutableWaiter()
desiredInstRef := b.lower.ResourceInstanceDesired(instAddrRef, waitFor)
return b.managedResourceInstanceSubgraphPlanAndApply(
desiredInstRef,
execgraph.NilResultRef[*exec.ResourceInstanceObject](),
plannedValRef,
providerClientRef,
), addConfigDep
}
func (b *execGraphBuilder) managedResourceInstanceSubgraphUpdate(
plannedChange *plans.ResourceInstanceChange,
providerClientRef execgraph.ResultRef[*exec.ProviderClient],
) (execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef)) {
instAddrRef, priorStateRef := b.managedResourceInstanceChangeAddrAndPriorStateRefs(plannedChange)
plannedValRef := b.lower.ConstantValue(plannedChange.After)
waitFor, addConfigDep := b.lower.MutableWaiter()
desiredInstRef := b.lower.ResourceInstanceDesired(instAddrRef, waitFor)
return b.managedResourceInstanceSubgraphPlanAndApply(
desiredInstRef,
priorStateRef,
plannedValRef,
providerClientRef,
), addConfigDep
}
// managedResourceInstanceSubgraphPlanAndApply deals with the simple case
// of "create a final plan and then apply it" that is shared between "create"
// and "update", but not for deleting or for the more complicated ones involving
// multiple primitive actions that need to be carefully coordinated with each
// other.
func (b *execGraphBuilder) managedResourceInstanceSubgraphPlanAndApply(
desiredInstRef execgraph.ResultRef[*eval.DesiredResourceInstance],
priorStateRef execgraph.ResourceInstanceResultRef,
plannedValRef execgraph.ResultRef[cty.Value],
providerClientRef execgraph.ResultRef[*exec.ProviderClient],
) execgraph.ResourceInstanceResultRef {
finalPlanRef := b.lower.ManagedFinalPlan(
desiredInstRef,
priorStateRef,
plannedValRef,
providerClientRef,
)
return b.lower.ManagedApply(
finalPlanRef,
execgraph.NilResultRef[*exec.ResourceInstanceObject](),
providerClientRef,
b.lower.Waiter(), // nothing to wait for: desiredInstRef should have the relevant dependencies itself
)
}
func (b *execGraphBuilder) managedResourceInstanceSubgraphDelete(
plannedChange *plans.ResourceInstanceChange,
providerClientRef execgraph.ResultRef[*exec.ProviderClient],
) (execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef)) {
_, priorStateRef := b.managedResourceInstanceChangeAddrAndPriorStateRefs(plannedChange)
plannedValRef := b.lower.ConstantValue(plannedChange.After)
waitFor, addDeleteDep := b.lower.MutableWaiter()
finalPlanRef := b.lower.ManagedFinalPlan(
execgraph.NilResultRef[*eval.DesiredResourceInstance](),
priorStateRef,
plannedValRef,
providerClientRef,
)
return b.lower.ManagedApply(
finalPlanRef,
execgraph.NilResultRef[*exec.ResourceInstanceObject](),
providerClientRef,
waitFor,
), addDeleteDep
}
func (b *execGraphBuilder) managedResourceInstanceSubgraphForget(
plannedChange *plans.ResourceInstanceChange,
providerClientRef execgraph.ResultRef[*exec.ProviderClient],
) execgraph.ResourceInstanceResultRef {
// TODO: Add a new execgraph opcode ManagedForget and use that here.
panic("execgraph for Forget not yet implemented")
}
func (b *execGraphBuilder) managedResourceInstanceSubgraphDeleteOrForgetThenCreate(
plannedChange *plans.ResourceInstanceChange,
providerClientRef execgraph.ResultRef[*exec.ProviderClient],
) (execgraph.ResourceInstanceResultRef, execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef), func(execgraph.AnyResultRef)) {
if plannedChange.Action == plans.ForgetThenCreate {
// TODO: Implement this action too, which is similar but with the
// "delete" let replaced with something like what
// managedResourceInstanceSubgraphForget would generate.
panic("execgraph for ForgetThenCreate not yet implemented")
}
desiredWaitFor, addConfigDep := b.lower.MutableWaiter()
deleteWaitFor, addDeleteDep := b.lower.MutableWaiter()
// This has much the same _effect_ as the separate delete and create
// actions chained together, but we arrange the operations in such a
// way that the delete leg can't start unless the desired state is
// successfully evaluated.
instAddrRef, priorStateRef := b.managedResourceInstanceChangeAddrAndPriorStateRefs(plannedChange)
plannedValRef := b.lower.ConstantValue(plannedChange.After)
desiredInstRef := b.lower.ResourceInstanceDesired(instAddrRef, desiredWaitFor)
// We plan both the create and destroy parts of this process before we
// make any real changes, to reduce the risk that we'll be left in a
// partially-applied state where neither object exists. (Though of course
// that's always possible, if the "create" step fails at apply.)
createPlanRef := b.lower.ManagedFinalPlan(
desiredInstRef,
execgraph.NilResultRef[*exec.ResourceInstanceObject](),
plannedValRef,
providerClientRef,
)
destroyPlanRef := b.lower.ManagedFinalPlan(
execgraph.NilResultRef[*eval.DesiredResourceInstance](),
priorStateRef,
b.lower.ConstantValue(cty.NullVal(
// TODO: is this okay or do we need to use the type constraint derived from the schema?
// The two could differ for resource types that have cty.DynamicPseudoType
// attributes, like in kubernetes_manifest from the hashicorp/kubernetes provider,
// where here we'd capture the type of the current manifest instead of recording
// that the manifest's type is unknown. However, we don't typically fuss too much
// about the exact type of a null, so this is probably fine.
plannedChange.After.Type(),
)),
providerClientRef,
)
destroyResultRef := b.lower.ManagedApply(
destroyPlanRef,
execgraph.NilResultRef[*exec.ResourceInstanceObject](),
providerClientRef,
deleteWaitFor,
)
createResultRef := b.lower.ManagedApply(
createPlanRef,
execgraph.NilResultRef[*exec.ResourceInstanceObject](),
providerClientRef,
b.lower.Waiter(destroyResultRef),
)
return createResultRef, destroyResultRef, addConfigDep, addDeleteDep
}
func (b *execGraphBuilder) managedResourceInstanceSubgraphCreateThenDelete(
plannedChange *plans.ResourceInstanceChange,
providerClientRef execgraph.ResultRef[*exec.ProviderClient],
) (execgraph.ResourceInstanceResultRef, execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef), func(execgraph.AnyResultRef)) {
desiredWaitFor, addConfigDep := b.lower.MutableWaiter()
deleteWaitFor, addDeleteDep := b.lower.MutableWaiter()
// This has much the same effect as the separate delete and create
// actions chained together, but we arrange the operations in such a
// way that we don't make any changes unless we can produce valid final
// plans for both changes.
instAddrRef, priorStateRef := b.managedResourceInstanceChangeAddrAndPriorStateRefs(plannedChange)
plannedValRef := b.lower.ConstantValue(plannedChange.After)
desiredInstRef := b.lower.ResourceInstanceDesired(instAddrRef, desiredWaitFor)
// We plan both the create and destroy parts of this process before we
// make any real changes, to reduce the risk that we'll be left in a
// partially-applied state where we're left with a deposed object present
// in the final state.
createPlanRef := b.lower.ManagedFinalPlan(
desiredInstRef,
execgraph.NilResultRef[*exec.ResourceInstanceObject](),
plannedValRef,
providerClientRef,
)
destroyPlanRef := b.lower.ManagedFinalPlan(
execgraph.NilResultRef[*eval.DesiredResourceInstance](),
priorStateRef,
b.lower.ConstantValue(cty.NullVal(
// TODO: is this okay or do we need to use the type constraint derived from the schema?
// The two could differ for resource types that have cty.DynamicPseudoType
// attributes, like in kubernetes_manifest from the hashicorp/kubernetes provider,
// where here we'd capture the type of the current manifest instead of recording
// that the manifest's type is unknown. However, we don't typically fuss too much
// about the exact type of a null, so this is probably fine.
plannedChange.After.Type(),
)),
providerClientRef,
)
deposedObjRef := b.lower.ManagedDepose(
priorStateRef,
b.lower.Waiter(createPlanRef, destroyPlanRef),
)
createResultRef := b.lower.ManagedApply(
createPlanRef,
deposedObjRef, // will be restored as current if creation completely fails
providerClientRef,
b.lower.Waiter(),
)
// No other resource instances can depend on the value from the destroy
// result, so if the destroy fails after the create succeeded then we can
// proceed with applying any downstream changes that refer to what we
// created and then we'll end with the deposed object still in the state and
// error diagnostics explaining why destroying it didn't work.
addDeleteDep(createResultRef) // delete must not begin until creation has succeeded
deletionRef := b.lower.ManagedApply(
destroyPlanRef,
execgraph.NilResultRef[*exec.ResourceInstanceObject](),
providerClientRef,
deleteWaitFor,
)
return createResultRef, deletionRef, addConfigDep, addDeleteDep
}
func (b *execGraphBuilder) managedResourceInstanceChangeAddrAndPriorStateRefs(
plannedChange *plans.ResourceInstanceChange,
) (
newAddr execgraph.ResultRef[addrs.AbsResourceInstance],
priorState execgraph.ResourceInstanceResultRef,
) {
if plannedChange.Action == plans.Create {
// For a create change there is no prior state at all, but we still
// need the new instance address.
newAddrRef := b.lower.ConstantResourceInstAddr(plannedChange.Addr)
return newAddrRef, execgraph.NilResultRef[*exec.ResourceInstanceObject]()
}
if plannedChange.DeposedKey != states.NotDeposed {
// We need to use a different operation to access deposed objects.
prevAddrRef := b.lower.ConstantResourceInstAddr(plannedChange.PrevRunAddr)
dkRef := b.lower.ConstantDeposedKey(plannedChange.DeposedKey)
stateRef := b.lower.ManagedAlreadyDeposed(prevAddrRef, dkRef)
return execgraph.NilResultRef[addrs.AbsResourceInstance](), stateRef
}
prevAddrRef := b.lower.ConstantResourceInstAddr(plannedChange.PrevRunAddr)
priorStateRef := b.lower.ResourceInstancePrior(prevAddrRef)
retAddrRef := prevAddrRef
retStateRef := priorStateRef
if !plannedChange.PrevRunAddr.Equal(plannedChange.Addr) {
// If the address is changing then we'll also include the
// "change address" operation so that the object will get rebound
// to its new address before we do any other work.
retAddrRef = b.lower.ConstantResourceInstAddr(plannedChange.Addr)
retStateRef = b.lower.ManagedChangeAddr(retStateRef, retAddrRef)
}
return retAddrRef, retStateRef
}