Files
opentf/internal/engine/planning/plan_replace_order.go
RoseSecurity 6e2250e050 chore: typo fixes (#3957)
Signed-off-by: RoseSecurity <michael@rosesecurity.dev>
2026-03-27 18:25:56 -03:00

160 lines
6.7 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/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/plans"
)
// findEffectiveReplaceOrders analyzes the given graph of resource instance
// objects to decide the final effective "replace order" for each resource
// instance object.
//
// Specifically, any object whose initial replace order is [replaceAnyOrder]
// will have its effective order set to either [replaceDestroyThenCreate] or
// [replaceCreateThenDestroy], depending on whether they are in dependency
// chains with objects those initial replace order was
// [replaceCreateThenDestroy]. All objects in a chain of dependencies are
// required to have the same replace order.
//
// The second return value is a set of addresses of objects which depend on
// themselves either directly or indirectly, which should be impossible if the
// graph was constructed correctly. If that set contains any elements then the
// map of effective replace orders is likely to be incomplete.
//
// This function currently assumes that all of the provided objects have their
// initial replace order set to either [replaceAnyOrder] or
// [replaceCreateThenDestroy]. If any of the given objects have the initial
// order [replaceDestroyThenCreate] then this function will panic; that
// replace order is used only as the _effective_ replace order for any object
// that isn't chained with an object whose initial order is
// [replaceCreateThenDestroy]. This models the current constraints of the
// surface language where "create_before_destroy = true" is treated as
// [replaceCreateThenDestroy] and everything else is treated as
// [replaceAnyOrder].
func findEffectiveReplaceOrders(objs *resourceInstanceObjects) (addrs.Map[addrs.AbsResourceInstanceObject, resourceInstanceReplaceOrder], addrs.Set[addrs.AbsResourceInstanceObject]) {
orders := addrs.MakeMap[addrs.AbsResourceInstanceObject, resourceInstanceReplaceOrder]()
selfDeps := addrs.MakeSet[addrs.AbsResourceInstanceObject]()
// This initial implementation is pretty simplistic: we just visit every
// object in an arbitrary order and then visit each of its dependencies
// and dependents in an arbitrary order until either we find one that
// requires "create then destroy" or until we run out of objects to check.
//
// Maybe later we'll devise a cleverer algorithm for this which doesn't
// involve revisiting the same objects quite as much. For now our only
// minor optimization is to stop as soon as we find the first neighbor
// with [replaceCreateThenDestroy].
Objects:
for currentInst, currentObj := range objs.All() {
if currentObj.ReplaceOrder == replaceCreateThenDestroy {
// Easy case: this one is definitely create-then-destroy.
orders.Put(currentInst, replaceCreateThenDestroy)
continue
}
for otherInst := range objs.DependenciesAndDependents(currentInst) {
if otherInst.Equal(currentInst) {
// We've found a self-dependency problem, so we'll record
// it but continue anyway because the rest of this algorithm
// can tolerate that sitution for now.
selfDeps.Add(currentInst)
continue
}
if currentObj.ReplaceOrder != replaceAnyOrder && currentObj.ReplaceOrder != replaceCreateThenDestroy {
panic(fmt.Sprintf("%s has invalid initial replace order %s", currentInst, currentObj.ReplaceOrder))
}
// If we've already recorded a decision for this one then we'll
// prefer to use that decision. At this point in the process that
// decision can only be [replaceCreateThenDestroy], because we
// don't populate any others until after these loops are complete.
if previous, ok := orders.GetOk(otherInst); ok {
orders.Put(currentInst, previous)
continue Objects
}
otherObj := objs.Get(otherInst)
if otherObj == nil {
// Can potentially happen if the resource instance graph is
// incomplete, such as if the input config was invalid. In
// that case we're just making a best effort to finalize a
// partial plan, so we'll ignore the invalid item.
continue
}
if otherObj.ReplaceOrder == replaceCreateThenDestroy {
orders.Put(currentInst, replaceCreateThenDestroy)
}
}
}
// Now we'll make a followup pass and just set everything we didn't already
// decide to replaceDestroyThenCreate, which is the default.
for currentInst := range objs.All() {
if !orders.Has(currentInst) {
orders.Put(currentInst, replaceDestroyThenCreate)
}
}
return orders, selfDeps
}
// resourceInstanceReplaceOrder represents the constraint, if any, for what
// order the create and destroy steps of a "replace" action must happen in.
type resourceInstanceReplaceOrder int
//go:generate go tool golang.org/x/tools/cmd/stringer -type=resourceInstanceReplaceOrder -trimprefix=replace
const (
// replaceAnyOrder means that it's okay to use either order, in which
// case the associated resource instance will just follow the prevailing
// order chosen by its upstream and downstream dependencies.
//
// It isn't possible for conflicting replace orders to coexist in the
// same chain of dependent resource instances because that would mean there
// is no valid order to perform the steps in, and so we rely on the
// assumption that most resource instances begin without any constraint
// and then just follow whatever order is required to satisfy the needs
// of their neighbors.
replaceAnyOrder resourceInstanceReplaceOrder = iota
// replaceCreateThenDestroy represents that a replacement object must be
// created before destroying the previous object.
replaceCreateThenDestroy
// replaceDestroyThenCreate represents that the previous object must be
// destroyed before creating its replacement, such as if both objects
// would try to occupy the same unique object name and so cannot coexist
// at the same time.
//
// This is the default resolution if all dependencies in a chain start
// off as [replaceAnyOrder].
replaceDestroyThenCreate
)
// ChangeAction returns the [plans.Action] corresponding to the receiver,
// or panics if the receiver is [replaceAnyOrder] because that value represents
// that we haven't yet decided which action to use.
//
// This should typically be used only on values taken from the result of a
// call to [findEffectiveReplaceOrders], where all resource instance objects
// are expected to have a definitive effective replace order.
func (o resourceInstanceReplaceOrder) ChangeAction() plans.Action {
switch o {
case replaceCreateThenDestroy:
return plans.CreateThenDelete
case replaceDestroyThenCreate:
return plans.DeleteThenCreate
default:
panic(fmt.Errorf("no change action for undecided replace order"))
}
}