mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-21 18:56:57 -05:00
This sketches the main happy path of planning for a desired managed resource instance, though doesn't include all of the possible variations that authors can cause with different configuration settings. The main goal here is just to illustrate how the planning step might interact with the work done in the eval system. If we decide to go in this direction then we'll need to bring over all of the complexity from the traditional runtime into this codepath, or possibly find some way to reuse that behavior directory from "package tofu" in the short term. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
384 lines
16 KiB
Go
384 lines
16 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 (
|
|
"context"
|
|
"fmt"
|
|
"iter"
|
|
"log"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/collections"
|
|
"github.com/opentofu/opentofu/internal/lang/eval"
|
|
"github.com/opentofu/opentofu/internal/plans/objchange"
|
|
"github.com/opentofu/opentofu/internal/providers"
|
|
"github.com/opentofu/opentofu/internal/states"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
// planGlue is our implementation of [eval.PlanGlue], which the evaluation
|
|
// system uses to help the planning engine drive the planning process forward
|
|
// as it learns information from the configuration.
|
|
//
|
|
// The methods of this type can all be called concurrently with themselves and
|
|
// each other, so they must use appropriate synchronization to avoid races.
|
|
type planGlue struct {
|
|
planCtx *planContext
|
|
oracle *eval.PlanningOracle
|
|
}
|
|
|
|
var _ eval.PlanGlue = (*planGlue)(nil)
|
|
|
|
// PlanDesiredResourceInstance implements eval.PlanGlue.
|
|
//
|
|
// This is called each time the evaluation system discovers a new resource
|
|
// instance in the configuration, and there are likely to be multiple calls
|
|
// active concurrently and so this function must take care to avoid races.
|
|
func (p *planGlue) PlanDesiredResourceInstance(ctx context.Context, inst *eval.DesiredResourceInstance) (cty.Value, tfdiags.Diagnostics) {
|
|
log.Printf("[TRACE] planContext: planning desired resource instance %s", inst.Addr)
|
|
// The details of how we plan vary considerably depending on the resource
|
|
// mode, so we'll dispatch each one to a separate function before we do
|
|
// anything else.
|
|
switch mode := inst.Addr.Resource.Resource.Mode; mode {
|
|
case addrs.ManagedResourceMode:
|
|
return p.planDesiredManagedResourceInstance(ctx, inst)
|
|
case addrs.DataResourceMode:
|
|
return p.planDesiredDataResourceInstance(ctx, inst)
|
|
case addrs.EphemeralResourceMode:
|
|
return p.planDesiredEphemeralResourceInstance(ctx, inst)
|
|
default:
|
|
// We should not get here because the cases above should always be
|
|
// exhaustive for all of the valid resource modes.
|
|
var diags tfdiags.Diagnostics
|
|
diags = diags.Append(fmt.Errorf("the planning engine does not support %s; this is a bug in OpenTofu", mode))
|
|
return cty.DynamicVal, diags
|
|
}
|
|
|
|
}
|
|
|
|
func (p *planGlue) planOrphanResourceInstance(ctx context.Context, addr addrs.AbsResourceInstance, state *states.ResourceInstance) tfdiags.Diagnostics {
|
|
log.Printf("[TRACE] planContext: planning orphan resource instance %s", addr)
|
|
switch mode := addr.Resource.Resource.Mode; mode {
|
|
case addrs.ManagedResourceMode:
|
|
return p.planOrphanManagedResourceInstance(ctx, addr, state)
|
|
case addrs.DataResourceMode:
|
|
return p.planOrphanDataResourceInstance(ctx, addr, state)
|
|
case addrs.EphemeralResourceMode:
|
|
// It should not be possible for an ephemeral resource to be an
|
|
// orphan because ephemeral resources should never be persisted
|
|
// in a state snapshot.
|
|
var diags tfdiags.Diagnostics
|
|
diags = diags.Append(fmt.Errorf("unexpected ephemeral resource instance %s in prior state; this is a bug in OpenTofu", addr))
|
|
return diags
|
|
default:
|
|
// We should not get here because the cases above should always be
|
|
// exhaustive for all of the valid resource modes.
|
|
var diags tfdiags.Diagnostics
|
|
diags = diags.Append(fmt.Errorf("the planning engine does not support %s; this is a bug in OpenTofu", mode))
|
|
return diags
|
|
}
|
|
}
|
|
|
|
func (p *planGlue) planDeposedResourceInstanceObject(ctx context.Context, addr addrs.AbsResourceInstance, deposedKey states.DeposedKey, state *states.ResourceInstance) tfdiags.Diagnostics {
|
|
log.Printf("[TRACE] planContext: planning deposed resource instance object %s %s", addr, deposedKey)
|
|
if addr.Resource.Resource.Mode != addrs.ManagedResourceMode {
|
|
// Should not be possible because only managed resource instances
|
|
// support "replace" and so nothing else can have deposed objects.
|
|
var diags tfdiags.Diagnostics
|
|
diags = diags.Append(fmt.Errorf("deposed object for non-managed resource instance %s; this is a bug in OpenTofu", addr))
|
|
return diags
|
|
}
|
|
return p.planDeposedManagedResourceInstanceObject(ctx, addr, deposedKey, state)
|
|
}
|
|
|
|
// PlanModuleCallInstanceOrphans implements eval.PlanGlue.
|
|
func (p *planGlue) PlanModuleCallInstanceOrphans(ctx context.Context, moduleCallAddr addrs.AbsModuleCall, desiredInstances iter.Seq[addrs.InstanceKey]) tfdiags.Diagnostics {
|
|
if moduleCallAddr.Module.IsPlaceholder() {
|
|
// can't predict anything about what might be desired or orphaned
|
|
// under this module instance.
|
|
// FIXME: _Something_ still needs to make sure we call
|
|
// p.planCtx.reportResourceInstancePlanCompletion for any
|
|
// potentially-matching instances in the previous round state, because
|
|
// nothing in the desired state is going to match them and so they
|
|
// won't actually get planned.
|
|
return nil
|
|
}
|
|
desiredSet := collections.CollectSet(desiredInstances)
|
|
for key := range desiredSet {
|
|
if _, ok := key.(addrs.WildcardKey); ok {
|
|
// can't predict what instances are desired for this module call
|
|
return nil
|
|
}
|
|
}
|
|
|
|
orphaned := resourceInstancesFilter(p.planCtx.prevRoundState, func(instAddr addrs.AbsResourceInstance) bool {
|
|
// This should return true for any resource instance in the given
|
|
// module instance that belongs to a module call not included in
|
|
// desiredCalls, and false otherwise.
|
|
if instAddr.Module.IsRoot() {
|
|
// A resource in the root module cannot possibly belong to a
|
|
// module call.
|
|
return false
|
|
}
|
|
instCallerModuleInstAddr, instModuleCallInstance := instAddr.Module.CallInstance()
|
|
if !instCallerModuleInstAddr.Equal(moduleCallAddr.Module) {
|
|
return false // not in the relevant calling module instance
|
|
}
|
|
if !instModuleCallInstance.Call.Equal(moduleCallAddr.Call) {
|
|
return false // not in the relevant module call
|
|
}
|
|
if desiredSet.Has(instModuleCallInstance.Key) {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
var diags tfdiags.Diagnostics
|
|
for addr, state := range orphaned {
|
|
diags = diags.Append(
|
|
p.planOrphanResourceInstance(ctx, addr, state),
|
|
)
|
|
}
|
|
return diags
|
|
}
|
|
|
|
// PlanModuleCallOrphans implements eval.PlanGlue.
|
|
func (p *planGlue) PlanModuleCallOrphans(ctx context.Context, callerModuleInstAddr addrs.ModuleInstance, desiredCalls iter.Seq[addrs.ModuleCall]) tfdiags.Diagnostics {
|
|
if callerModuleInstAddr.IsPlaceholder() {
|
|
// can't predict anything about what might be desired or orphaned
|
|
// under this module instance.
|
|
// FIXME: _Something_ still needs to make sure we call
|
|
// p.planCtx.reportResourceInstancePlanCompletion for any
|
|
// potentially-matching instances in the previous round state, because
|
|
// nothing in the desired state is going to match them and so they
|
|
// won't actually get planned.
|
|
return nil
|
|
}
|
|
desiredSet := addrs.CollectSet(desiredCalls)
|
|
|
|
orphaned := resourceInstancesFilter(p.planCtx.prevRoundState, func(instAddr addrs.AbsResourceInstance) bool {
|
|
// This should return true for any resource instance in the given
|
|
// module instance that belongs to a module call not included in
|
|
// desiredCalls, and false otherwise.
|
|
if instAddr.Module.IsRoot() {
|
|
// A resource in the root module cannot possibly belong to a
|
|
// module call.
|
|
return false
|
|
}
|
|
instCallerModuleInstAddr, instModuleCall := instAddr.Module.Call()
|
|
if !instCallerModuleInstAddr.Equal(callerModuleInstAddr) {
|
|
return false // not in the relevant module instance
|
|
}
|
|
if desiredSet.Has(instModuleCall) {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
var diags tfdiags.Diagnostics
|
|
for addr, state := range orphaned {
|
|
diags = diags.Append(
|
|
p.planOrphanResourceInstance(ctx, addr, state),
|
|
)
|
|
}
|
|
return diags
|
|
}
|
|
|
|
// PlanResourceInstanceOrphans implements eval.PlanGlue.
|
|
func (p *planGlue) PlanResourceInstanceOrphans(ctx context.Context, resourceAddr addrs.AbsResource, desiredInstances iter.Seq[addrs.InstanceKey]) tfdiags.Diagnostics {
|
|
if resourceAddr.IsPlaceholder() {
|
|
// can't predict anything about what might be desired or orphaned
|
|
// under this resource.
|
|
// FIXME: _Something_ still needs to make sure we call
|
|
// p.planCtx.reportResourceInstancePlanCompletion for any
|
|
// potentially-matching instances in the previous round state, because
|
|
// nothing in the desired state is going to match them and so they
|
|
// won't actually get planned.
|
|
return nil
|
|
}
|
|
desiredSet := collections.CollectSet(desiredInstances)
|
|
for key := range desiredSet {
|
|
if _, ok := key.(addrs.WildcardKey); ok {
|
|
// can't predict what instances are desired for this resource
|
|
return nil
|
|
}
|
|
}
|
|
|
|
orphaned := resourceInstancesFilter(p.planCtx.prevRoundState, func(instAddr addrs.AbsResourceInstance) bool {
|
|
// This should return true for any resource instance in the given
|
|
// module instance that belongs to a resource not included in
|
|
// desiredResources, and false otherwise.
|
|
if !instAddr.Module.Equal(resourceAddr.Module) {
|
|
return false // not in the relevant module instance
|
|
}
|
|
if !instAddr.Resource.Resource.Equal(resourceAddr.Resource) {
|
|
return false // not in the relevant resource
|
|
}
|
|
if desiredSet.Has(instAddr.Resource.Key) {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
var diags tfdiags.Diagnostics
|
|
for addr, state := range orphaned {
|
|
diags = diags.Append(
|
|
p.planOrphanResourceInstance(ctx, addr, state),
|
|
)
|
|
}
|
|
return diags
|
|
}
|
|
|
|
// PlanResourceOrphans implements eval.PlanGlue.
|
|
func (p *planGlue) PlanResourceOrphans(ctx context.Context, moduleInstAddr addrs.ModuleInstance, desiredResources iter.Seq[addrs.Resource]) tfdiags.Diagnostics {
|
|
if moduleInstAddr.IsPlaceholder() {
|
|
// can't predict anything about what might be desired or orphaned
|
|
// under this resource instance.
|
|
// FIXME: _Something_ still needs to make sure we call
|
|
// p.planCtx.reportResourceInstancePlanCompletion for any
|
|
// potentially-matching instances in the previous round state, because
|
|
// nothing in the desired state is going to match them and so they
|
|
// won't actually get planned.
|
|
return nil
|
|
}
|
|
desiredSet := addrs.CollectSet(desiredResources)
|
|
|
|
orphaned := resourceInstancesFilter(p.planCtx.prevRoundState, func(addr addrs.AbsResourceInstance) bool {
|
|
// This should return true for any resource instance in the given
|
|
// module instance that belongs to a resource not included in
|
|
// desiredResources, and false otherwise.
|
|
if !addr.Module.Equal(moduleInstAddr) {
|
|
return false // not in the relevant module instance
|
|
}
|
|
if desiredSet.Has(addr.Resource.Resource) {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
var diags tfdiags.Diagnostics
|
|
for addr, state := range orphaned {
|
|
diags = diags.Append(
|
|
p.planOrphanResourceInstance(ctx, addr, state),
|
|
)
|
|
}
|
|
return diags
|
|
}
|
|
|
|
// ProviderClient returns a client for the requested provider instance, launching
|
|
// and configuring the provider first if no caller has previously requested a
|
|
// client for this instance.
|
|
//
|
|
// Returns nil if the configuration for the requested provider instance is too
|
|
// invalid to actually configure it. The diagnostics for such a problem would
|
|
// be reported by our main [ConfigInstance.DrivePlanning] call but the caller
|
|
// of this function will probably want to return a more specialized error saying
|
|
// that the corresponding resource cannot be planned because its associated
|
|
// provider has an invalid configuration.
|
|
func (p *planGlue) providerClient(ctx context.Context, addr addrs.AbsProviderInstanceCorrect) (providers.Configured, tfdiags.Diagnostics) {
|
|
return p.planCtx.providerInstances.ProviderClient(ctx, addr, p)
|
|
}
|
|
|
|
// providerInstanceCompletionEvents returns all of the [completionEvent] values
|
|
// that need to have been reported to the completion tracker before an
|
|
// instance of the given provider can be closed.
|
|
func (p *planGlue) providerInstanceCompletionEvents(ctx context.Context, addr addrs.AbsProviderInstanceCorrect) iter.Seq[completionEvent] {
|
|
return func(yield func(completionEvent) bool) {
|
|
configUsers := p.oracle.ProviderInstanceUsers(ctx, addr)
|
|
for _, resourceInstAddr := range configUsers.ResourceInstances {
|
|
event := resourceInstancePlanningComplete{resourceInstAddr.UniqueKey()}
|
|
if !yield(event) {
|
|
return
|
|
}
|
|
}
|
|
// We also need to wait for the completion of anything we can find
|
|
// in the state, just in case any resource instances are "orphaned"
|
|
// and in case there are any deposed objects we need to deal with.
|
|
for _, modState := range p.planCtx.prevRoundState.Modules {
|
|
for _, resourceState := range modState.Resources {
|
|
for instKey, instanceState := range resourceState.Instances {
|
|
resourceInstAddr := resourceState.Addr.Instance(instKey)
|
|
// FIXME: State is still using the not-quite-right address
|
|
// types for provider instances, so we'll shim here.
|
|
providerInstAddr := resourceState.ProviderConfig.InstanceCorrect(instanceState.ProviderKey)
|
|
if !addr.Equal(providerInstAddr) {
|
|
continue // not for this provider instance
|
|
}
|
|
if instanceState.Current != nil {
|
|
event := resourceInstancePlanningComplete{resourceInstAddr.UniqueKey()}
|
|
if !yield(event) {
|
|
return
|
|
}
|
|
}
|
|
for dk := range instanceState.Deposed {
|
|
event := resourceInstanceDeposedPlanningComplete{resourceInstAddr.UniqueKey(), dk}
|
|
if !yield(event) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *planGlue) desiredResourceInstanceMustBeDeferred(inst *eval.DesiredResourceInstance) bool {
|
|
// There are various reasons why we might need to defer final planning
|
|
// of this to a later round. The following is not exhaustive but is a
|
|
// placeholder to show where deferral might fit in.
|
|
return inst.IsPlaceholder() || inst.ProviderInstance == nil || derivedFromDeferredVal(inst.ConfigVal)
|
|
}
|
|
|
|
func (p *planGlue) resourceInstancePlaceholderValue(ctx context.Context, providerAddr addrs.Provider, resourceMode addrs.ResourceMode, resourceType string, priorVal, configVal cty.Value) cty.Value {
|
|
evalCtx := p.oracle.EvalContext(ctx)
|
|
schema, diags := evalCtx.Providers.ResourceTypeSchema(ctx, providerAddr, resourceMode, resourceType)
|
|
if diags.HasErrors() {
|
|
// If we can't get any schema information then we'll just return
|
|
// a completely-unknown object as our placeholder. We should get here
|
|
// only if the eval system already failed to use the provider to decode
|
|
// or validate the configuration, and so it should already have reported
|
|
// a related error upstream.
|
|
return cty.DynamicVal
|
|
}
|
|
|
|
if configVal.IsNull() {
|
|
return cty.NullVal(schema.Block.ImpliedType().WithoutOptionalAttributesDeep())
|
|
}
|
|
if !configVal.IsKnown() {
|
|
return cty.UnknownVal(schema.Block.ImpliedType().WithoutOptionalAttributesDeep())
|
|
}
|
|
return objchange.ProposedNew(
|
|
schema.Block,
|
|
priorVal,
|
|
configVal,
|
|
)
|
|
}
|
|
|
|
// resourceInstancesFilter returns a sequence of resource instances from the
|
|
// given state whose addresses caused the "want" function to return true.
|
|
//
|
|
// This is an inefficient way to implement detection of "orphans" with our
|
|
// current state model. If we decide to adopt a design like this then we
|
|
// should adopt a different representation of state which uses a tree structure
|
|
// where we can efficiently scan over subtrees that match a particular prefix,
|
|
// rather than always scanning over everything.
|
|
func resourceInstancesFilter(state *states.State, want func(addrs.AbsResourceInstance) bool) iter.Seq2[addrs.AbsResourceInstance, *states.ResourceInstance] {
|
|
return func(yield func(addrs.AbsResourceInstance, *states.ResourceInstance) bool) {
|
|
for _, modState := range state.Modules {
|
|
for _, resourceState := range modState.Resources {
|
|
for instKey, instanceState := range resourceState.Instances {
|
|
instAddr := resourceState.Addr.Instance(instKey)
|
|
if !want(instAddr) {
|
|
continue
|
|
}
|
|
if !yield(instAddr, instanceState) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|