exec: ManagedDepose and ManagedChangeAddr take object as operand

These both effectively had the behavior of ResourceInstancePrior embedded
in them, reading something from the state and change its address as a
single compound operation.

In the case of ManagedDepose we need to split these up for the
CreateThenDestroy variant of "replace", because we want to make sure the
final plans are valid before we depose anything and we need the prior state
to produce the final plan. (Actually using that will follow in a subsequent
commit.)

This isn't actually necessary for ManageChangeAddr, but splitting it keeps
these two operations consistent in how they interact with the rest of the
operations.

Due to how the existing states.SyncState works we're not actually making
good use of the data flow of these objects right now, but in a future world
where we're no longer using the old state models hopefully the state API
will switch to an approach that's more aligned with how the execgraph
operations are modeled.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Martin Atkins 2026-02-09 14:53:27 -08:00
parent 766be2c7fb
commit ac9a26d5b9
9 changed files with 160 additions and 73 deletions

View file

@ -311,18 +311,27 @@ func (ops *execOperations) ManagedApply(
// ManagedDepose implements [exec.Operations].
func (ops *execOperations) ManagedDepose(
ctx context.Context,
instAddr addrs.AbsResourceInstance,
currentObj *exec.ResourceInstanceObject,
) (*exec.ResourceInstanceObject, tfdiags.Diagnostics) {
log.Printf("[TRACE] apply phase: ManagedDepose %s", instAddr)
var diags tfdiags.Diagnostics
deposedKey := ops.workingState.DeposeResourceInstanceObject(instAddr)
if deposedKey == states.NotDeposed {
// This means that there was no "current" object to depose, and
// so we'll return nil to represent that there's nothing here.
if currentObj == nil {
log.Println("[TRACE] apply phase: ManagedDepose with nil object (ignored)")
return nil, diags
}
return ops.resourceInstanceStateObject(ctx, ops.workingState, instAddr, deposedKey)
log.Printf("[TRACE] apply phase: ManagedDepose %s", currentObj.InstanceAddr)
deposedKey := ops.workingState.DeposeResourceInstanceObject(currentObj.InstanceAddr)
if deposedKey == states.NotDeposed {
// We should not get here with a correctly-constructed execution graph
// because currentObj being non-nil means that there should definitely
// be something to depose.
diags = diags.Append(fmt.Errorf(
"failed to depose the current object for %s; this is a bug in OpenTofu",
currentObj.InstanceAddr,
))
return nil, diags
}
return currentObj.IntoDeposed(deposedKey), diags
}
// ManagedAlreadyDeposed implements [exec.Operations].
@ -341,9 +350,24 @@ func (ops *execOperations) ManagedAlreadyDeposed(
// ManagedChangeAddr implements [exec.Operations].
func (ops *execOperations) ManagedChangeAddr(
ctx context.Context,
currentInstAddr, newInstAddr addrs.AbsResourceInstance,
currentObj *exec.ResourceInstanceObject,
newAddr addrs.AbsResourceInstance,
) (*exec.ResourceInstanceObject, tfdiags.Diagnostics) {
log.Printf("[TRACE] apply phase: ManagedChangeAddr from %s to %s", currentInstAddr, newInstAddr)
// TODO: Implement
panic("ManagedChangeAddr not yet implemented")
var diags tfdiags.Diagnostics
if currentObj == nil {
log.Println("[TRACE] apply phase: ManagedChangeAddr with nil object (ignored)")
return nil, diags
}
log.Printf("[TRACE] apply phase: ManagedChangeAddr from %s to %s", currentObj.InstanceAddr, newAddr)
if !ops.workingState.MaybeMoveResourceInstance(currentObj.InstanceAddr, newAddr) {
// We should not get here with a correctly-constructed execution graph
// because currentObj being non-nil means that there should definitely
// be something to move.
diags = diags.Append(fmt.Errorf(
"failed to move %s to %s; this is a bug in OpenTofu",
currentObj.InstanceAddr, newAddr,
))
return nil, diags
}
return currentObj.WithNewAddr(newAddr), diags
}

View file

@ -191,21 +191,26 @@ type Operations interface {
providerClient *ProviderClient,
) (*ResourceInstanceObject, tfdiags.Diagnostics)
// ManagedDepose transforms the "current" object associated with the given
// resource instance address into a "deposed" object for the same resource
// instance, and then returns the description of the now-deposed object.
//
// If there is no current object associated with that resource instance,
// this returns nil without changing anything.
// ManagedDepose takes a "current" object for some resource instance and
// changes it to be a "deposed" object for the same resource instance,
// returning a new representation of the object with its
// pseudorandomly-chosen unique DeposedKey.
//
// When using this as part of a "create then destroy" replace operation,
// a correct execution graph arranges for the result to be propagated into
// the "fallback" argument of a subsequent [Operations.ManagedApply] call,
// so that the deposed object can be restored back to current if the
// apply operation fails to the extent that no new object is created at all.
//
// The given object must not already have "DeposedKey" set, because that
// would make it a deposed object instead of a current object.
// If the given object is nil then this returns nil without changing
// anything. In practice though the planning engine should not include
// this operation unless it found an existing current object that needs to
// be deposed as part of a create-then-destroy "replace" change.
ManagedDepose(
ctx context.Context,
instAddr addrs.AbsResourceInstance,
object *ResourceInstanceObject,
) (*ResourceInstanceObject, tfdiags.Diagnostics)
// ManagedAlreadyDeposed returns a deposed object from the prior state,
@ -217,6 +222,9 @@ type Operations interface {
// That occurs only when a previous plan/apply round encountered an error
// partway through a "create then destroy" replace operation where both
// the newly-created object and the previously-existing object still exist.
// In that case, this operation serves a similar purpose to
// [Operations.ResourceInstancePrior] but returns a deposed object rather
// than a current object.
//
// [Operations.ManagedDepose] deals with the more common case where a
// previously-"current" object becomes deposed during the apply phase as
@ -227,25 +235,28 @@ type Operations interface {
deposedKey states.DeposedKey,
) (*ResourceInstanceObject, tfdiags.Diagnostics)
// ManageChangeAddr rebinds the current object associated with
// currentInstAddr to be associated with newInstAddr instead, and then
// returns that object with its updated address.
// ManageChangeAddr rebinds the given object to be associated with
// newInstAddr instead, and then returns a new representation of that object
// with its updated address.
//
// This is used in place of [Operations.ResourceInstancePrior] whenever a
// resource instance address is being moved to a new address. The move
// and the read from the state are combined into a single action so that
// we can treat this as an atomic operation where there's no intermediate
// state where the relevant object is associated with either neither or both
// of the two addresses.
// This is used between [Operations.ResourceInstancePrior] and
// [Operations.ManagedFinalPlan] whenever an existing resource instance
// object is being moved to a new address using "moved" blocks. The move
// is modelled as a separate action because it's okay for the final state
// to reflect the address change even if subsequent plan/apply actions
// fail.
//
// If there is no current object associated with currentInstAddr when
// this operation executes then it does nothing and returns a nil object
// with no errors, though in practice the planning engine should not include
// this operation unless it found an existing object that needed to be
// moved.
// If the incoming object is nil then this also returns nil without making
// any change and no errors. In practice though the planning engine should
// not include this operation unless it found an existing object that needed
// to be moved.
//
// This is for use with "current" resource instance objects only, so
// implementers can assume that the given object will have no DeposedKey.
ManagedChangeAddr(
ctx context.Context,
currentInstAddr, newInstAddr addrs.AbsResourceInstance,
object *ResourceInstanceObject,
newAddr addrs.AbsResourceInstance,
) (*ResourceInstanceObject, tfdiags.Diagnostics)
//////////////////////////////////////////////////////////////////////////////

View file

@ -117,6 +117,16 @@ func (o *ResourceInstanceObject) IntoDeposed(key states.DeposedKey) *ResourceIns
}
}
// WithNewAddr returns a new [ResourceInstanceObject] that has the same
// State as the receiver but has InstanceAddr set to the given address.
func (o *ResourceInstanceObject) WithNewAddr(addr addrs.AbsResourceInstance) *ResourceInstanceObject {
return &ResourceInstanceObject{
InstanceAddr: addr,
DeposedKey: o.DeposedKey,
State: o.State,
}
}
// IntoCurrent returns a new [ResourceInstanceObject] that has the same
// address information as the receiver but has State set to the given object.
//

View file

@ -193,11 +193,12 @@ func (b *Builder) ManagedApply(
}
func (b *Builder) ManagedDepose(
instAddr ResultRef[addrs.AbsResourceInstance],
currentObj ResourceInstanceResultRef,
waitFor AnyResultRef,
) ResourceInstanceResultRef {
return operationRef[*exec.ResourceInstanceObject](b, operationDesc{
opCode: opManagedDepose,
operands: []AnyResultRef{instAddr},
operands: []AnyResultRef{currentObj, waitFor},
})
}
@ -212,12 +213,12 @@ func (b *Builder) ManagedAlreadyDeposed(
}
func (b *Builder) ManagedChangeAddr(
currentInstAddr ResultRef[addrs.AbsResourceInstance],
newInstAddr ResultRef[addrs.AbsResourceInstance],
currentObj ResourceInstanceResultRef,
newAddr ResultRef[addrs.AbsResourceInstance],
) ResourceInstanceResultRef {
return operationRef[*exec.ResourceInstanceObject](b, operationDesc{
opCode: opManagedChangeAddr,
operands: []AnyResultRef{currentInstAddr, newInstAddr},
operands: []AnyResultRef{currentObj, newAddr},
})
}

View file

@ -231,7 +231,8 @@ func (c *compiler) compileOpManagedApply(operands *compilerOperands) nodeExecute
func (c *compiler) compileOpManagedDepose(operands *compilerOperands) nodeExecuteRaw {
ops := c.ops
getInstAddr := nextOperand[addrs.AbsResourceInstance](operands)
getCurrentObj := nextOperand[*exec.ResourceInstanceObject](operands)
waitForDeps := operands.OperandWaiter()
diags := operands.Finish()
c.diags = c.diags.Append(diags)
if diags.HasErrors() {
@ -240,14 +241,17 @@ func (c *compiler) compileOpManagedDepose(operands *compilerOperands) nodeExecut
return func(ctx context.Context) (any, bool, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
if !waitForDeps(ctx) {
return nil, false, diags
}
instAddr, ok, moreDiags := getInstAddr(ctx)
currentObj, ok, moreDiags := getCurrentObj(ctx)
diags = diags.Append(moreDiags)
if !ok {
return nil, false, diags
}
ret, moreDiags := ops.ManagedDepose(ctx, instAddr)
ret, moreDiags := ops.ManagedDepose(ctx, currentObj)
diags = diags.Append(moreDiags)
return ret, !diags.HasErrors(), diags
}
@ -285,8 +289,8 @@ func (c *compiler) compileOpManagedAlreadyDeposed(operands *compilerOperands) no
func (c *compiler) compileOpManagedChangeAddr(operands *compilerOperands) nodeExecuteRaw {
ops := c.ops
getCurrentInstAddr := nextOperand[addrs.AbsResourceInstance](operands)
getNewInstAddr := nextOperand[addrs.AbsResourceInstance](operands)
getCurrentObj := nextOperand[*exec.ResourceInstanceObject](operands)
getNewAddr := nextOperand[addrs.AbsResourceInstance](operands)
diags := operands.Finish()
c.diags = c.diags.Append(diags)
if diags.HasErrors() {
@ -296,18 +300,18 @@ func (c *compiler) compileOpManagedChangeAddr(operands *compilerOperands) nodeEx
return func(ctx context.Context) (any, bool, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
currentInstAddr, ok, moreDiags := getCurrentInstAddr(ctx)
currentObj, ok, moreDiags := getCurrentObj(ctx)
diags = diags.Append(moreDiags)
if !ok {
return nil, false, diags
}
newInstAddr, ok, moreDiags := getNewInstAddr(ctx)
newAddr, ok, moreDiags := getNewAddr(ctx)
diags = diags.Append(moreDiags)
if !ok {
return nil, false, diags
}
ret, moreDiags := ops.ManagedChangeAddr(ctx, currentInstAddr, newInstAddr)
ret, moreDiags := ops.ManagedChangeAddr(ctx, currentObj, newAddr)
diags = diags.Append(moreDiags)
return ret, !diags.HasErrors(), diags
}

View file

@ -271,14 +271,18 @@ func unmarshalOpManagedApply(rawOperands []uint64, prevResults []AnyResultRef, b
}
func unmarshalOpManagedDepose(rawOperands []uint64, prevResults []AnyResultRef, builder *Builder) (AnyResultRef, error) {
if len(rawOperands) != 1 {
if len(rawOperands) != 2 {
return nil, fmt.Errorf("wrong number of operands (%d) for opManagedDepose", len(rawOperands))
}
instAddr, err := unmarshalGetPrevResultOf[addrs.AbsResourceInstance](prevResults, rawOperands[0])
currentObj, err := unmarshalGetPrevResultOf[*exec.ResourceInstanceObject](prevResults, rawOperands[0])
if err != nil {
return nil, fmt.Errorf("invalid opManagedDepose instAddr: %w", err)
return nil, fmt.Errorf("invalid opManagedDepose currentObj: %w", err)
}
return builder.ManagedDepose(instAddr), nil
waitFor, err := unmarshalGetPrevResultWaiter(prevResults, rawOperands[1])
if err != nil {
return nil, fmt.Errorf("invalid opManagedDepose waitFor: %w", err)
}
return builder.ManagedDepose(currentObj, waitFor), nil
}
func unmarshalOpManagedAlreadyDeposed(rawOperands []uint64, prevResults []AnyResultRef, builder *Builder) (AnyResultRef, error) {
@ -300,15 +304,15 @@ func unmarshalOpManagedChangeAddr(rawOperands []uint64, prevResults []AnyResultR
if len(rawOperands) != 2 {
return nil, fmt.Errorf("wrong number of operands (%d) for opManagedChangeAddr", len(rawOperands))
}
currentInstAddr, err := unmarshalGetPrevResultOf[addrs.AbsResourceInstance](prevResults, rawOperands[0])
currentObj, err := unmarshalGetPrevResultOf[*exec.ResourceInstanceObject](prevResults, rawOperands[0])
if err != nil {
return nil, fmt.Errorf("invalid opManagedChangeAddr currentInstAddr: %w", err)
return nil, fmt.Errorf("invalid opManagedChangeAddr currentObj: %w", err)
}
newInstAddr, err := unmarshalGetPrevResultOf[addrs.AbsResourceInstance](prevResults, rawOperands[1])
newAddr, err := unmarshalGetPrevResultOf[addrs.AbsResourceInstance](prevResults, rawOperands[1])
if err != nil {
return nil, fmt.Errorf("invalid opManagedChangeAddr newInstAddr: %w", err)
}
return builder.ManagedChangeAddr(currentInstAddr, newInstAddr), nil
return builder.ManagedChangeAddr(currentObj, newAddr), nil
}
func unmarshalOpDataRead(rawOperands []uint64, prevResults []AnyResultRef, builder *Builder) (AnyResultRef, error) {

View file

@ -28,8 +28,8 @@ type mockOperations struct {
EphemeralStateFunc func(ctx context.Context, ephemeral *exec.OpenEphemeralResourceInstance) (*exec.ResourceInstanceObject, tfdiags.Diagnostics)
ManagedAlreadyDeposedFunc func(ctx context.Context, instAddr addrs.AbsResourceInstance, deposedKey states.DeposedKey) (*exec.ResourceInstanceObject, tfdiags.Diagnostics)
ManagedApplyFunc func(ctx context.Context, plan *exec.ManagedResourceObjectFinalPlan, fallback *exec.ResourceInstanceObject, providerClient *exec.ProviderClient) (*exec.ResourceInstanceObject, tfdiags.Diagnostics)
ManagedChangeAddrFunc func(ctx context.Context, currentInstAddr, newInstAddr addrs.AbsResourceInstance) (*exec.ResourceInstanceObject, tfdiags.Diagnostics)
ManagedDeposeFunc func(ctx context.Context, instAddr addrs.AbsResourceInstance) (*exec.ResourceInstanceObject, tfdiags.Diagnostics)
ManagedChangeAddrFunc func(ctx context.Context, currentObj *exec.ResourceInstanceObject, newAddr addrs.AbsResourceInstance) (*exec.ResourceInstanceObject, tfdiags.Diagnostics)
ManagedDeposeFunc func(ctx context.Context, currentObj *exec.ResourceInstanceObject) (*exec.ResourceInstanceObject, tfdiags.Diagnostics)
ManagedFinalPlanFunc func(ctx context.Context, desired *eval.DesiredResourceInstance, prior *exec.ResourceInstanceObject, plannedVal cty.Value, providerClient *exec.ProviderClient) (*exec.ManagedResourceObjectFinalPlan, tfdiags.Diagnostics)
ProviderInstanceCloseFunc func(ctx context.Context, client *exec.ProviderClient) tfdiags.Diagnostics
ProviderInstanceConfigFunc func(ctx context.Context, instAddr addrs.AbsProviderInstanceCorrect) (*exec.ProviderInstanceConfig, tfdiags.Diagnostics)
@ -109,24 +109,24 @@ func (m *mockOperations) ManagedApply(ctx context.Context, plan *exec.ManagedRes
}
// ManagedChangeAddr implements [exec.Operations].
func (m *mockOperations) ManagedChangeAddr(ctx context.Context, currentInstAddr, newInstAddr addrs.AbsResourceInstance) (*exec.ResourceInstanceObject, tfdiags.Diagnostics) {
func (m *mockOperations) ManagedChangeAddr(ctx context.Context, currentObj *exec.ResourceInstanceObject, newAddr addrs.AbsResourceInstance) (*exec.ResourceInstanceObject, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var result *exec.ResourceInstanceObject
if m.ManagedChangeAddrFunc != nil {
result, diags = m.ManagedChangeAddrFunc(ctx, currentInstAddr, newInstAddr)
result, diags = m.ManagedChangeAddrFunc(ctx, currentObj, newAddr)
}
m.appendLog("ManagedChangeAddr", []any{currentInstAddr, newInstAddr}, result)
m.appendLog("ManagedChangeAddr", []any{currentObj, newAddr}, result)
return result, diags
}
// ManagedDepose implements [exec.Operations].
func (m *mockOperations) ManagedDepose(ctx context.Context, instAddr addrs.AbsResourceInstance) (*exec.ResourceInstanceObject, tfdiags.Diagnostics) {
func (m *mockOperations) ManagedDepose(ctx context.Context, currentObj *exec.ResourceInstanceObject) (*exec.ResourceInstanceObject, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var result *exec.ResourceInstanceObject
if m.ManagedDeposeFunc != nil {
result, diags = m.ManagedDeposeFunc(ctx, instAddr)
result, diags = m.ManagedDeposeFunc(ctx, currentObj)
}
m.appendLog("ManagedDepose", []any{instAddr}, result)
m.appendLog("ManagedDepose", []any{currentObj}, result)
return result, diags
}

View file

@ -255,7 +255,6 @@ func (b *execGraphBuilder) managedResourceInstanceChangeAddrAndPriorStateRefs(
newAddrRef := b.lower.ConstantResourceInstAddr(plannedChange.Addr)
return newAddrRef, execgraph.NilResultRef[*exec.ResourceInstanceObject]()
}
prevAddrRef := b.lower.ConstantResourceInstAddr(plannedChange.PrevRunAddr)
if plannedChange.DeposedKey != states.NotDeposed {
// We need to use a different operation to access deposed objects.
prevAddrRef := b.lower.ConstantResourceInstAddr(plannedChange.PrevRunAddr)
@ -263,15 +262,16 @@ func (b *execGraphBuilder) managedResourceInstanceChangeAddrAndPriorStateRefs(
stateRef := b.lower.ManagedAlreadyDeposed(prevAddrRef, dkRef)
return execgraph.NilResultRef[addrs.AbsResourceInstance](), stateRef
}
newAddrRef := b.lower.ConstantResourceInstAddr(plannedChange.Addr)
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 need to use the "change address"
// operation instead of just reading te prior state.
stateRef := b.lower.ManagedChangeAddr(prevAddrRef, newAddrRef)
return newAddrRef, stateRef
// 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)
}
// In all other cases we just take the prior state directly out of the
// prior state, without any special behavior.
stateRef := b.lower.ResourceInstancePrior(prevAddrRef)
return newAddrRef, stateRef
return retAddrRef, retStateRef
}

View file

@ -92,6 +92,39 @@ func TestExecGraphBuilder_ManagedResourceInstanceSubgraph(t *testing.T) {
test.placeholder = r[3];
`,
},
"update with move": {
func(b *execGraphBuilder, providerClientRef execgraph.ResultRef[*exec.ProviderClient]) execgraph.ResourceInstanceResultRef {
oldInstAddr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test",
Name: "old",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
return b.ManagedResourceInstanceSubgraph(
&plans.ResourceInstanceChange{
Addr: instAddr,
PrevRunAddr: oldInstAddr,
Change: plans.Change{
Action: plans.Update,
Before: cty.StringVal("before"),
After: cty.StringVal("after"),
},
},
providerClientRef,
addrs.MakeSet[addrs.AbsResourceInstance](),
)
},
`
v[0] = cty.StringVal("after");
r[0] = ResourceInstancePrior(test.old);
r[1] = ManagedChangeAddr(r[0], test.placeholder);
r[2] = ResourceInstanceDesired(test.placeholder, await());
r[3] = ManagedFinalPlan(r[2], r[1], v[0], nil);
r[4] = ManagedApply(r[3], nil, nil, await());
test.placeholder = r[4];
`,
},
"delete": {
func(b *execGraphBuilder, providerClientRef execgraph.ResultRef[*exec.ProviderClient]) execgraph.ResourceInstanceResultRef {
return b.ManagedResourceInstanceSubgraph(