mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-03-20 22:01:25 -04:00
We'll now remember what configuration dependencies we found when we instantiated each provider instance during the planning phase and include the same explicit dependencies in the execution graph. We still have an open question of what to do with ephemeral dependencies such as those between provider instances and ephemeral resources, since those are allowed vary between plan and apply. There are existing TODOs about that in the ephemeral resource planning codepaths, but for now we're focused mainly on managed resource instances for our "walking skeleton" milestone and so we'll slightly-incorrectly assume that the dependencies will be the same during the apply phase for now until we complete that later planned design work. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
398 lines
18 KiB
Go
398 lines
18 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"
|
|
"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
|
|
|
|
// Dependencies 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.
|
|
//
|
|
// For orphan or deposed objects which therefore appear only in state and
|
|
// have no current configured dependencies, this should describe all of
|
|
// the resource instance objects in the prior state that the state object
|
|
// was recorded as depending on, instead of dependencies detected through
|
|
// the configuration. We assume that the dependencies recorded in the state
|
|
// match what was declared in an earlier version of the configuration.
|
|
Dependencies 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 supports concurrent writes and 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]
|
|
|
|
// reverseDeps describes the same relationships as in
|
|
// [resourceInstanceObject.Dependencies] 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.
|
|
reverseDeps addrs.Map[addrs.AbsResourceInstanceObject, addrs.Set[addrs.AbsResourceInstanceObject]]
|
|
|
|
// providerInstDeps describes the resource instances that each provider
|
|
// instance depends on, which are therefore effectively additional indirect
|
|
// dependencies for any resource instance that belongs to a given
|
|
// provider instance and must be represented in the final execution graph.
|
|
providerInstDeps addrs.Map[addrs.AbsProviderInstanceCorrect, addrs.Set[addrs.AbsResourceInstance]]
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Dependencies returns the addresses of all resource instance objects that the
|
|
// resource instance object of the given address depends on.
|
|
//
|
|
// The caller must not modify any object reachable through the results.
|
|
func (rios *resourceInstanceObjects) Dependencies(of addrs.AbsResourceInstanceObject) iter.Seq[addrs.AbsResourceInstanceObject] {
|
|
obj := rios.objects.Get(of)
|
|
if obj == nil {
|
|
return func(yield func(addrs.AbsResourceInstanceObject) bool) {}
|
|
}
|
|
return obj.Dependencies.All()
|
|
}
|
|
|
|
// Dependents returns the addresses of all resource instance objects that have
|
|
// the resource instance object with the given address as one of their
|
|
// dependencies. In other words, this queries the 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) Dependendents(of addrs.AbsResourceInstanceObject) iter.Seq[addrs.AbsResourceInstanceObject] {
|
|
return rios.reverseDeps.Get(of).All()
|
|
}
|
|
|
|
// DependenciesAndDependents is a convenience helper that concatenates together
|
|
// the results of both [resourceInstanceObjects.Dependencies] and
|
|
// [resourceInstanceObjects.Dependents] into a single flat sequence. It does
|
|
// not transform those sequences in any other way.
|
|
func (rios *resourceInstanceObjects) DependenciesAndDependents(of addrs.AbsResourceInstanceObject) iter.Seq[addrs.AbsResourceInstanceObject] {
|
|
return func(yield func(addrs.AbsResourceInstanceObject) bool) {
|
|
for addr := range rios.Dependencies(of) {
|
|
if !yield(addr) {
|
|
return
|
|
}
|
|
}
|
|
for addr := range rios.Dependendents(of) {
|
|
if !yield(addr) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ProviderInstanceDependencies returns the addresses of all resource instance
|
|
// objects that the given provider instance depends on to be configured.
|
|
//
|
|
// The caller must not modify any object reachable through the results.
|
|
func (rios *resourceInstanceObjects) ProviderInstanceDependencies(of addrs.AbsProviderInstanceCorrect) iter.Seq[addrs.AbsResourceInstanceObject] {
|
|
// We internally track these as relationships to resource instances rather
|
|
// than resource instance objects because provider instances are not allowed
|
|
// to depend on deposed objects, but for consistency with the other methods
|
|
// of this type we'll translate them into resource instance object addresses
|
|
// referring to the current object of each instance as we stream them out.
|
|
return func(yield func(addrs.AbsResourceInstanceObject) bool) {
|
|
for _, instAddr := range rios.providerInstDeps.Get(of) {
|
|
if !yield(instAddr.CurrentObject()) {
|
|
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](),
|
|
reverseDeps: addrs.MakeMap[addrs.AbsResourceInstanceObject, addrs.Set[addrs.AbsResourceInstanceObject]](),
|
|
providerInstDeps: addrs.MakeMap[addrs.AbsProviderInstanceCorrect, addrs.Set[addrs.AbsResourceInstance]](),
|
|
},
|
|
}
|
|
}
|
|
|
|
// 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 reverseDeps structure here to ensure that our
|
|
// records of the graph edges are always consistent across both directions.
|
|
for depAddr := range obj.Dependencies.All() {
|
|
if !b.result.reverseDeps.Has(depAddr) {
|
|
b.result.reverseDeps.Put(depAddr, addrs.MakeSet[addrs.AbsResourceInstanceObject]())
|
|
}
|
|
b.result.reverseDeps.Get(depAddr).Add(obj.Addr)
|
|
}
|
|
}
|
|
|
|
// PutProviderInstanceDependencies records the set of resource instance objects
|
|
// that the given provider instance depends on in order to be configured.
|
|
//
|
|
// This takes a set of provider instance addresses rather than provider instance
|
|
// _object_ addresses because it's never valid for a provider instance to
|
|
// depend on deposed resource instance objects.
|
|
//
|
|
// This function panics if there have been multiple calls for the same provider
|
|
// instance. The caller must not modify the given set or anything reachable
|
|
// through it after calling this function, because it becomes part of the
|
|
// internal state of the reciever.
|
|
func (b *resourceInstanceObjectsBuilder) PutProviderInstanceDependencies(addr addrs.AbsProviderInstanceCorrect, deps addrs.Set[addrs.AbsResourceInstance]) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
if b.result.providerInstDeps.Has(addr) {
|
|
panic(fmt.Sprintf("%s dependencies already tracked in this resourceInstanceObjects collection", addr))
|
|
}
|
|
b.result.providerInstDeps.Put(addr, deps)
|
|
|
|
// We don't retain reverse dependencies for provider instances, because
|
|
// they are never "destroyed" and so backward edges are never needed.
|
|
}
|
|
|
|
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)
|
|
}
|