opentofu/internal/engine/planning/execgraph_managed.go
Martin Atkins 94968c2165 execgraph: ManagedPerformDepose now takes a delete plan
Previously ManagedPerformDeposed just allocated a fresh deposed key
dynamically at apply time, but to allow it to compose with the
recently-added ManagePrepareDeposed it will now take a "delete" plan as
an argument and expect it to already be annotated with a unique deposed
key to use.

This means that we will now preselect all of the new deposed keys during
the planning phase, making it easier to relate the generated execution
graph to the results of running it during the apply phase.

This commit also includes changes to the planning engine to now generate
a correct execution subgraph for create-then-destroy replace operations,
where the final step of destroying the old object now correctly destroys
the deposed object instead of getting confused and overwriting the current
object with the result of the destroy.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2026-06-04 17:19:45 -07:00

336 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,
) (
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)
case plans.Update:
valueRef, addConfigDep = b.managedResourceInstanceSubgraphUpdate(plannedChange)
case plans.Delete:
deletionRef, addDeleteDep = b.managedResourceInstanceSubgraphDelete(plannedChange)
case plans.Forget:
valueRef = b.managedResourceInstanceSubgraphForget(plannedChange)
case plans.DeleteThenCreate, plans.ForgetThenCreate:
valueRef, deletionRef, addConfigDep, addDeleteDep = b.managedResourceInstanceSubgraphDeleteOrForgetThenCreate(plannedChange)
case plans.CreateThenDelete:
valueRef, deletionRef, addConfigDep, addDeleteDep = b.managedResourceInstanceSubgraphCreateThenDelete(plannedChange)
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,
) (execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef)) {
instAddrRef, _ := b.managedResourceInstanceChangeAddrAndPriorStateRefs(plannedChange)
// Per the conventions in the old engine, After contains a marked value
unmarkedAfter, _ := plannedChange.After.UnmarkDeep()
plannedValRef := b.lower.ConstantValue(unmarkedAfter)
waitFor, addConfigDep := b.lower.MutableWaiter()
desiredInstRef := b.lower.ResourceInstanceDesired(instAddrRef, waitFor)
return b.managedResourceInstanceSubgraphPlanAndApply(
desiredInstRef,
execgraph.NilResultRef[*exec.ResourceInstanceObject](),
plannedValRef,
), addConfigDep
}
func (b *execGraphBuilder) managedResourceInstanceSubgraphUpdate(
plannedChange *plans.ResourceInstanceChange,
) (execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef)) {
instAddrRef, priorStateRef := b.managedResourceInstanceChangeAddrAndPriorStateRefs(plannedChange)
// Per the conventions in the old engine, After contains a marked value
unmarkedAfter, _ := plannedChange.After.UnmarkDeep()
plannedValRef := b.lower.ConstantValue(unmarkedAfter)
waitFor, addConfigDep := b.lower.MutableWaiter()
desiredInstRef := b.lower.ResourceInstanceDesired(instAddrRef, waitFor)
return b.managedResourceInstanceSubgraphPlanAndApply(
desiredInstRef,
priorStateRef,
plannedValRef,
), 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],
) execgraph.ResourceInstanceResultRef {
finalPlanRef := b.lower.ManagedFinalPlan(
desiredInstRef,
priorStateRef,
plannedValRef,
)
return b.lower.ManagedApply(
finalPlanRef,
execgraph.NilResultRef[*exec.ResourceInstanceObject](),
b.lower.Waiter(), // nothing to wait for: desiredInstRef should have the relevant dependencies itself
)
}
func (b *execGraphBuilder) managedResourceInstanceSubgraphDelete(
plannedChange *plans.ResourceInstanceChange,
) (execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef)) {
_, priorStateRef := b.managedResourceInstanceChangeAddrAndPriorStateRefs(plannedChange)
// Per the conventions in the old engine, After contains a marked value
unmarkedAfter, _ := plannedChange.After.UnmarkDeep()
plannedValRef := b.lower.ConstantValue(unmarkedAfter)
waitFor, addDeleteDep := b.lower.MutableWaiter()
finalPlanRef := b.lower.ManagedFinalPlan(
execgraph.NilResultRef[*eval.DesiredResourceInstance](),
priorStateRef,
plannedValRef,
)
return b.lower.ManagedApply(
finalPlanRef,
execgraph.NilResultRef[*exec.ResourceInstanceObject](),
waitFor,
), addDeleteDep
}
func (b *execGraphBuilder) managedResourceInstanceSubgraphForget(
plannedChange *plans.ResourceInstanceChange,
) 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,
) (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)
// Per the conventions in the old engine, After contains a marked value
unmarkedAfter, _ := plannedChange.After.UnmarkDeep()
plannedValRef := b.lower.ConstantValue(unmarkedAfter)
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,
)
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(),
)),
)
destroyResultRef := b.lower.ManagedApply(
destroyPlanRef,
execgraph.NilResultRef[*exec.ResourceInstanceObject](),
deleteWaitFor,
)
createResultRef := b.lower.ManagedApply(
createPlanRef,
execgraph.NilResultRef[*exec.ResourceInstanceObject](),
b.lower.Waiter(destroyResultRef),
)
return createResultRef, destroyResultRef, addConfigDep, addDeleteDep
}
func (b *execGraphBuilder) managedResourceInstanceSubgraphCreateThenDelete(
plannedChange *plans.ResourceInstanceChange,
) (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)
// Per the conventions in the old engine, After contains a marked value
unmarkedAfter, _ := plannedChange.After.UnmarkDeep()
plannedValRef := b.lower.ConstantValue(unmarkedAfter)
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,
)
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(),
)),
)
// We'll now reinterpret the destroy plan as being for the deposed key
// that we'll retain this object under until the new object has been
// created, or failed to be created.
deposedKey := b.lower.ConstantDeposedKey(b.makeDeposedKey(plannedChange.Addr))
destroyPlanRef = b.lower.ManagedPrepareDepose(destroyPlanRef, deposedKey)
deposedObjRef := b.lower.ManagedPerformDepose(
priorStateRef,
destroyPlanRef,
b.lower.Waiter(createPlanRef),
)
createResultRef := b.lower.ManagedApply(
createPlanRef,
deposedObjRef, // will be restored as current if creation completely fails
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](),
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
}