blob: 72926adbe39e01ebf6e1175becdffb5aa2203514 [file] [edit]
// 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"
"iter"
"slices"
"sync"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/plans"
)
// resourceInstanceObject is the planning engine's internal intermediate
// representation of a resource instance object that's participating in the
// planning process.
//
// Objects of this type form an intermediate "resource instance graph" that
// gathers together all of the information needed to produce the final
// "execution graph" that describes what ought to happen during a subsequent
// apply phase.
type resourceInstanceObject struct {
// Addr is the unique address of this object.
//
// If an object's address is expected to change during the apply phase,
// such as if it's subject to "moved" blocks, then this returns the expected
// final address after the changes have been made. The address from the
// prior state is recorded in the PlannedChange object.
//
// If the planned change has action [plans.CreateThenDelete] then the
// "old" object will be turned into a deposed object for the part of the
// operation where both new and old object need to exist concurrently. The
// deposed key for that temporary object is only calculated just in time
// during the apply phase and so is not recorded anywhere in
// [resourceInstanceObject].
Addr addrs.AbsResourceInstanceObject
// PlannedChange is the change that the planning engine has decided for
// this resource instance object.
//
// If no actual change is needed then this object should not be present,
// and the effective result value should be placed in the PlaceholderValue
// field. Objects without a planned change are included in the execution
// graph only if a planned change for another object refers to them, and
// in that case only as a read from the prior state, and so PlaceholderValue
// should represent what would be read from the prior state during apply.
//
// If the planned action is to "replace" (for managed resource instance
// objects) and the ReplaceOrder field is set to [replaceAnyOrder] then
// the planned action can be set to either [plans.DestroyThenCreate] or
// [plans.CreateThenDestroy] and then will be overridden once the
// replace order has been finalized in subsequent analysis. If ReplaceOrder
// is set to a specific value then the planned change's action must
// immediately agree with the selected order.
// TODO: As we consider adjusting the plan model to better suit this new
// runtime, hopefully we can have the plan action initially set to just
// a generic "replace" and then let ReplaceOrder alone specify which order
// to use, so that we can avoid this redundancy.
PlannedChange *plans.ResourceInstanceChange
// PlaceholderValue is the value that should be used to satisfy references
// to this object in expressions elsewhere in the configuration when
// the PlannedChange field contains a nil change.
//
// This MUST Be set to a valid value unless the PlannedChange field is
// populated. If PlannedChange is populated then this field is completely
// ignored.
//
// The placeholder value is a conservative approximation of what we know
// should definitely match a hypothetical successful plan for this object,
// so that downstream references can still be typechecked even when
// their upstreams could not complete planning. This allows us to give as
// much information as possible about an invalid configuration, but that
// relies on the placeholder having unknown values in any position where
// we cannot predict the final result. If we don't know anything at all
// about the final result then it's valid to use [cty.DynamicVal] here.
PlaceholderValue cty.Value
// Provider is the address of the provider the resource type of this
// object belongs to, and thus the provider whose schema we must use
// when interpreting the old, new, or placeholder values for this object.
//
// This must always be populated, even if we don't know exactly which
// instance of this provider is responsible for it. If PlannedChange is
// also populated then the provider instance recorded as being responsible
// for the change must belong to the same provider recorded here.
Provider addrs.Provider
// ProviderInst is a substitute for the ProviderAddr field of the object
// in the PlannedChange field, compensating for the fact that our old plan
// models use an incorrect and incomplete model of provider instance
// addresses.
//
// This must be valid whenever PlannedChange is non-nil. It's invalid and
// ignored otherwise, because conceptually this is a field of PlannedChange
// that's out here only so we can delay updating the plan model that the
// traditional runtime is also relying on.
//
// TODO: Remove this once our plan model is updated to use the "correct"
// representation of provider instance addresses.
ProviderInst addrs.AbsProviderInstanceCorrect
// ReplaceOrder specifies what order the create and destroy steps of a
// "replace" must happen in for this object.
//
// When returning from one of the planning functions on [planGlue] this
// should focus on describing only the configured constraints of the
// specific object in question, using [replaceAnyOrder] if there is no
// constraint.
//
// Subsequent processing elsewhere in the planning engine will decide
// a final effective constraint to replace any [replaceAnyOrder]
// constraints, by analyzing the dependency flow between objects.
//
// Currently the planning engine is allowed to return only either
// [replaceAnyOrder] or [replaceCreateThenDestroy] in this field.
// [replaceDestroyThenCreate] is then inferred automatically for any
// object that isn't forced to be [replaceCreateThenDestroy] by one of
// its dependency neighbors.
ReplaceOrder resourceInstanceReplaceOrder
// ConfigDependencies is the set of all resource instance objects that this
// object's resource instance depends on either directly or indirectly.
//
// Note that this describes the dependencies between the resource instance
// objects as declared or implied in configuration, NOT the ordering
// requirements of the specific change in the PlannedChange field. In
// particular, if the planned action is [plans.Delete] then this field must
// still record the normal dependencies of the resource instances whose
// object is being destroyed and NOT the "inverted" dependencies that would
// be reflected in the final execution graph.
//
// This should be empty for orphan or deposed objects which therefore appear
// only in state and have no current configured dependencies. State-based
// dependencies appear in StateDependencies instead.
ConfigDependencies addrs.Set[addrs.AbsResourceInstanceObject]
// StateDependencies is similar to
// [resourceInstanceObject.ConfigDependencies] but describes dependencies
// take from the previous round state instead of from the configuration.
// We assume that dependencies recorded here were based on configuration
// dependencies from the previous round.
//
// This should be empty for "create" because there is no previous round
// state in that case.
StateDependencies addrs.Set[addrs.AbsResourceInstanceObject]
}
// ResultValue returns the value that should be sent to the evaluator for
// use in resolving downstream expressions that refer to this resource instance
// object.
//
// If the PlannedChange field is populated then this returns its "After" value.
//
// Otherwise, this returns PlaceholderValue so that downstream planning can
// potentially still proceed based on partial information.
func (rio *resourceInstanceObject) ResultValue() cty.Value {
if rio.PlannedChange != nil {
return rio.PlannedChange.After
}
if rio.PlaceholderValue != cty.NilVal {
return rio.PlaceholderValue
}
// We should not get here for correctly-constructed objects, but for
// robustness we'll use cty.DynamicVal as the ultimate placeholder.
return cty.DynamicVal
}
// resourceInstanceObjects is conceptually a map from
// [addrs.AbsResourceInstanceObject] to [*resourceInstanceObject], but it also
// allows querying the dependency relationships between objects in both
// directions.
//
// Collections of this type and everything inside them should be treated as
// immutable. Use [newResourceInstanceObjectsBuilder] to obtain a temporary
// object for constructing a new objects collection, and then call
// [resourceInstanceObjectsBuilder.Close] to derive the final immutable
// collection from it.
type resourceInstanceObjects struct {
// objects are the resource instance objects that have been added so far.
objects addrs.Map[addrs.AbsResourceInstanceObject, *resourceInstanceObject]
// reverseConfigDeps describes the same relationships as in
// [resourceInstanceObject.ConfigDependencies] but viewed from the opposite
// direction: the map keys are dependencies of the objects in the map
// values.
//
// We maintain this so that we can efficiently traverse the graph in both
// directions when performing further analysis.
reverseConfigDeps addrs.Map[addrs.AbsResourceInstanceObject, addrs.Set[addrs.AbsResourceInstanceObject]]
// reverseConfigDeps describes the same relationships as in
// [resourceInstanceObject.StateDependencies] but viewed from the opposite
// direction: the map keys are dependencies of the objects in the map
// values.
//
// We maintain this so that we can efficiently traverse the graph in both
// directions when performing further analysis.
reverseStateDeps addrs.Map[addrs.AbsResourceInstanceObject, addrs.Set[addrs.AbsResourceInstanceObject]]
}
// Get returns the resource instance object with the given address, or nil if
// no such object has been added.
//
// The caller must not mutate anything accessible through the returned pointer.
func (rios *resourceInstanceObjects) Get(addr addrs.AbsResourceInstanceObject) *resourceInstanceObject {
return rios.objects.Get(addr)
}
// AllAddrs returns a sequence over all of the resource instance objects known
// to this collection.
func (rios *resourceInstanceObjects) All() iter.Seq2[addrs.AbsResourceInstanceObject, *resourceInstanceObject] {
return func(yield func(addrs.AbsResourceInstanceObject, *resourceInstanceObject) bool) {
for _, elem := range rios.objects.Elems {
if !yield(elem.Key, elem.Value) {
return
}
}
}
}
// ConfigDependencies returns the addresses of all resource instance objects
// that the configuration of the resource instance object at the given address
// depends on.
//
// The caller must not modify any object reachable through the results.
func (rios *resourceInstanceObjects) ConfigDependencies(of addrs.AbsResourceInstanceObject) iter.Seq[addrs.AbsResourceInstanceObject] {
obj := rios.objects.Get(of)
if obj == nil {
return func(yield func(addrs.AbsResourceInstanceObject) bool) {}
}
return obj.ConfigDependencies.All()
}
// StateDependencies returns the addresses of all resource instance objects that
// the previous round state for the resource instance object of the given
// address depends on.
//
// The caller must not modify any object reachable through the results.
func (rios *resourceInstanceObjects) StateDependencies(of addrs.AbsResourceInstanceObject) iter.Seq[addrs.AbsResourceInstanceObject] {
obj := rios.objects.Get(of)
if obj == nil {
return func(yield func(addrs.AbsResourceInstanceObject) bool) {}
}
return obj.StateDependencies.All()
}
// ConfigDependents returns the addresses of all resource instance objects that
// have the resource instance object with the given address as one of their
// config dependencies. In other words, this queries the config dependencies
// "backwards".
//
// The caller must not modify any object reachable through the results.
//
// Note that not all of the returned addresses necessarily match an object
// that can be retrieved using [resourceInstanceObjects.Get]. The collection
// of resource instance objects is populated in a "forward dependency" order,
// and so dependencies are added before their dependents and the dependents
// might not be added at all if the planning process failed partway through.
func (rios *resourceInstanceObjects) ConfigDependents(of addrs.AbsResourceInstanceObject) iter.Seq[addrs.AbsResourceInstanceObject] {
return rios.reverseConfigDeps.Get(of).All()
}
// StateDependents returns the addresses of all resource instance objects that
// have the resource instance object with the given address as one of their
// state dependencies. In other words, this queries the state dependencies
// "backwards".
//
// The caller must not modify any object reachable through the results.
//
// Note that not all of the returned addresses necessarily match an object
// that can be retrieved using [resourceInstanceObjects.Get]. The collection
// of resource instance objects is populated in a "forward dependency" order,
// and so dependencies are added before their dependents and the dependents
// might not be added at all if the planning process failed partway through.
func (rios *resourceInstanceObjects) StateDependents(of addrs.AbsResourceInstanceObject) iter.Seq[addrs.AbsResourceInstanceObject] {
return rios.reverseStateDeps.Get(of).All()
}
// ConfigDependenciesAndDependents is a convenience helper that concatenates
// together the results of both [resourceInstanceObjects.ConfigDependencies] and
// [resourceInstanceObjects.ConfigDependents] into a single flat sequence. It does
// not transform those sequences in any other way.
func (rios *resourceInstanceObjects) ConfigDependenciesAndDependents(of addrs.AbsResourceInstanceObject) iter.Seq[addrs.AbsResourceInstanceObject] {
return func(yield func(addrs.AbsResourceInstanceObject) bool) {
for addr := range rios.ConfigDependencies(of) {
if !yield(addr) {
return
}
}
for addr := range rios.ConfigDependents(of) {
if !yield(addr) {
return
}
}
}
}
// StateDependenciesAndDependents is a convenience helper that concatenates
// together the results of both [resourceInstanceObjects.StateDependencies] and
// [resourceInstanceObjects.StateDependents] into a single flat sequence. It does
// not transform those sequences in any other way.
func (rios *resourceInstanceObjects) StateDependenciesAndDependents(of addrs.AbsResourceInstanceObject) iter.Seq[addrs.AbsResourceInstanceObject] {
return func(yield func(addrs.AbsResourceInstanceObject) bool) {
for addr := range rios.StateDependencies(of) {
if !yield(addr) {
return
}
}
for addr := range rios.StateDependents(of) {
if !yield(addr) {
return
}
}
}
}
// AllDependents combines the results of
// [*resourceInstanceObjects.ConfigDependents] and
// [*resourceInstanceObjects.StateDependents] into a single
// flat sequence.
func (rios *resourceInstanceObjects) AllDependents(of addrs.AbsResourceInstanceObject) iter.Seq[addrs.AbsResourceInstanceObject] {
return func(yield func(addrs.AbsResourceInstanceObject) bool) {
for addr := range rios.ConfigDependents(of) {
if !yield(addr) {
return
}
}
for addr := range rios.StateDependents(of) {
if !yield(addr) {
return
}
}
}
}
// resourceInstanceObjectsBuilder is a wrapper around a
// [resourceInstanceObjects] that allows new objects to be inserted in a
// concurrency-safe way.
type resourceInstanceObjectsBuilder struct {
mu sync.Mutex
result *resourceInstanceObjects
}
func newResourceInstanceObjectsBuilder() *resourceInstanceObjectsBuilder {
return &resourceInstanceObjectsBuilder{
result: &resourceInstanceObjects{
objects: addrs.MakeMap[addrs.AbsResourceInstanceObject, *resourceInstanceObject](),
reverseConfigDeps: addrs.MakeMap[addrs.AbsResourceInstanceObject, addrs.Set[addrs.AbsResourceInstanceObject]](),
reverseStateDeps: addrs.MakeMap[addrs.AbsResourceInstanceObject, addrs.Set[addrs.AbsResourceInstanceObject]](),
},
}
}
func (b *resourceInstanceObjectsBuilder) Get(addr addrs.AbsResourceInstanceObject) (*resourceInstanceObject, bool) {
b.mu.Lock()
defer b.mu.Unlock()
return b.result.objects.GetOk(addr)
}
// Put inserts the given resource instance object into the collection.
//
// Resource instance objects are uniquely identified by the addresses
// in [resourceInstanceObject.Addr]. Attempting to add an object with the same
// address as a previously-added object causes a panic, because it suggests a
// bug in the caller.
//
// A [resourceInstanceObject] value must not be modified once it has been passed
// to this method.
func (b *resourceInstanceObjectsBuilder) Put(obj *resourceInstanceObject) {
b.mu.Lock()
defer b.mu.Unlock()
if b.result.objects.Has(obj.Addr) {
panic(fmt.Sprintf("%s is already tracked in this resourceInstanceObjects collection", obj.Addr))
}
b.result.objects.Put(obj.Addr, obj)
// We also update the reverse maps here to ensure that our records of the
// graph edges are always consistent across both directions.
for depAddr := range obj.ConfigDependencies.All() {
if !b.result.reverseConfigDeps.Has(depAddr) {
b.result.reverseConfigDeps.Put(depAddr, addrs.MakeSet[addrs.AbsResourceInstanceObject]())
}
b.result.reverseConfigDeps.Get(depAddr).Add(obj.Addr)
}
for depAddr := range obj.StateDependencies.All() {
if !b.result.reverseStateDeps.Has(depAddr) {
b.result.reverseStateDeps.Put(depAddr, addrs.MakeSet[addrs.AbsResourceInstanceObject]())
}
b.result.reverseStateDeps.Get(depAddr).Add(obj.Addr)
}
}
func (b *resourceInstanceObjectsBuilder) Close() *resourceInstanceObjects {
b.mu.Lock()
ret := b.result
b.result = nil // this builder can't be used anymore
b.mu.Unlock()
return ret
}
// sortedResourceInstanceObjectAddrs consumes all items from the given sequence
// and then sorts them into an unspecified but deterministic order.
func sortedResourceInstanceObjectAddrs(seq iter.Seq[addrs.AbsResourceInstanceObject]) []addrs.AbsResourceInstanceObject {
sorted := slices.Collect(seq)
slices.SortFunc(sorted, func(a, b addrs.AbsResourceInstanceObject) int {
// The types in package addrs were written in a time when
// sort.Interface was idiomatic and therefore have "Less" rather
// than "Equal", and so we'll adapt that to the modern Compare
// interface for now, which unfortunately means we need two
// comparisons.
// TODO: Add Compare-style methods to the address types that already
// implement Less, reimplement their Less methods in terms of compare,
// and then change this to just directly use
// addrs.AbsResourceInstanceObject.Compare .
if a.Less(b) {
return -1
}
if b.Less(a) {
return 1
}
return 0
})
return sorted
}
// sortedResourceInstanceObjectAddrs consumes all items from the given sequence
// and then sorts them into an unspecified but deterministic order.
func sortedResourceInstanceObjectAddrKeys[T any](seq iter.Seq2[addrs.AbsResourceInstanceObject, T]) []addrs.AbsResourceInstanceObject {
keys := func(yield func(addrs.AbsResourceInstanceObject) bool) {
for k := range seq {
if !yield(k) {
return
}
}
}
return sortedResourceInstanceObjectAddrs(keys)
}