mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-03-28 14:00:49 -04:00
Our new language runtime uses a set of new methods on SyncState to work with its preferred "full" representation of resource instance objects, but those are implemented in terms of methods that already existed for the old runtime's benefit and so we need to deal with some quirks of those existing methods. One such quirk is that the operations to write or remove objects also want to update some resource-level and instance-level metadata as a side-effect, and we need to carry through that metadata even when we're intending to completely remove a resource instance object. To preserve our goal of leaving the existing codepaths untouched for now, this pushes a little complexity back up into the main caller in the apply engine, forcing it to call a different method when it knows it has deleted an object. That new method then only takes the metadata we need and not an actual resource instance object, so it gels better with the underlying ModuleState methods it's implemented in terms of. Hopefully in the long run we'll rethink the state models to not rely on these hidden side-effects, but that's beyond the scope of our current phase of work on the new language runtime. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
477 lines
20 KiB
Go
477 lines
20 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"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/lang/eval"
|
|
"github.com/opentofu/opentofu/internal/plans"
|
|
"github.com/opentofu/opentofu/internal/resources"
|
|
"github.com/opentofu/opentofu/internal/states"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
func (p *planGlue) planDesiredManagedResourceInstance(
|
|
ctx context.Context,
|
|
inst *eval.DesiredResourceInstance,
|
|
) (ret *resourceInstanceObject, diags tfdiags.Diagnostics) {
|
|
|
|
// 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.
|
|
if p.desiredResourceInstanceMustBeDeferred(inst) {
|
|
p.planCtx.deferred.Put(inst.Addr, struct{}{})
|
|
defer func() {
|
|
// Our result must be marked as deferred, whichever return path
|
|
// we leave through.
|
|
if ret != nil && ret.PlannedChange.After != cty.NilVal {
|
|
ret.PlannedChange.After = deferredVal(ret.PlannedChange.After)
|
|
}
|
|
}()
|
|
// We intentionally continue anyway, because we'll make a best effort
|
|
// to produce a speculative plan based on the information we _do_ know
|
|
// in case that allows us to detect a problem sooner. The important
|
|
// thing is that in the deferred case we won't actually propose any
|
|
// planned changes for this resource instance.
|
|
}
|
|
|
|
ret = &resourceInstanceObject{
|
|
Addr: inst.Addr.CurrentObject(),
|
|
Dependencies: addrs.MakeSet[addrs.AbsResourceInstanceObject](),
|
|
Provider: inst.Provider,
|
|
|
|
// We'll start off with a completely-unknown placeholder value, but
|
|
// we might refine this to be more specific as we learn more below.
|
|
PlaceholderValue: cty.DynamicVal,
|
|
|
|
// NOTE: PlannedChange remains nil until we actually produce a plan,
|
|
// so early returns with errors are not guaranteed to have a valid
|
|
// change object. Evaluation falls back on using PlaceholderValue
|
|
// when no planned change is present.
|
|
}
|
|
for dep := range inst.RequiredResourceInstances.All() {
|
|
ret.Dependencies.Add(dep.CurrentObject())
|
|
}
|
|
if inst.CreateBeforeDestroy {
|
|
ret.ReplaceOrder = replaceCreateThenDestroy
|
|
}
|
|
|
|
if inst.ProviderInstance == nil {
|
|
// If we don't even know which provider instance we're supposed to be
|
|
// talking to then we can't proceed any further.
|
|
return ret, diags
|
|
}
|
|
providerClient, moreDiags := p.providerClient(ctx, *inst.ProviderInstance)
|
|
if providerClient == nil {
|
|
moreDiags = moreDiags.Append(tfdiags.AttributeValue(
|
|
tfdiags.Error,
|
|
"Provider instance not available",
|
|
fmt.Sprintf("Cannot plan %s because its associated provider instance %s cannot initialize.", inst.Addr, *inst.ProviderInstance),
|
|
nil,
|
|
))
|
|
}
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
return ret, diags
|
|
}
|
|
|
|
resourceType := resources.NewManagedResourceType(inst.Provider, inst.Addr.Resource.Resource.Type, providerClient)
|
|
schema, schemaDiags := resourceType.LoadSchema(ctx)
|
|
if schemaDiags.HasErrors() {
|
|
// We don't return the schema-loading diagnostics directly here because
|
|
// they should have already been returned by earlier code, but we do
|
|
// return a more specific error to make it clear that this specific
|
|
// resource instance was unplannable because of the problem.
|
|
diags = diags.Append(tfdiags.AttributeValue(
|
|
tfdiags.Error,
|
|
"Resource type schema unavailable",
|
|
fmt.Sprintf(
|
|
"Cannot plan %s because provider %s failed to return the schema for its resource type %q.",
|
|
inst.Addr, inst.Provider, inst.Addr.Resource.Resource.Type,
|
|
),
|
|
nil, // this error belongs to the whole resource config
|
|
))
|
|
return ret, diags
|
|
}
|
|
|
|
validateDiags := resourceType.ValidateConfig(ctx, inst.ConfigVal)
|
|
diags = diags.Append(validateDiags)
|
|
if diags.HasErrors() {
|
|
return ret, diags
|
|
}
|
|
|
|
var prevRoundVal cty.Value
|
|
var prevRoundPrivate []byte
|
|
prevRoundState := p.planCtx.prevRoundState.SyncWrapper().ResourceInstanceObjectFull(inst.Addr.CurrentObject())
|
|
if prevRoundState != nil {
|
|
obj, err := states.DecodeResourceInstanceObjectFull(prevRoundState, schema.Block.ImpliedType())
|
|
if err != nil {
|
|
diags = diags.Append(tfdiags.AttributeValue(
|
|
tfdiags.Error,
|
|
"Invalid prior state for resource instance",
|
|
fmt.Sprintf(
|
|
"Cannot decode the most recent state snapshot for %s: %s.\n\nIs the selected version of %s incompatible with the provider that most recently changed this object?",
|
|
inst.Addr, tfdiags.FormatError(err), inst.Provider,
|
|
),
|
|
nil, // this error belongs to the whole resource config
|
|
))
|
|
return ret, diags
|
|
}
|
|
prevRoundVal = obj.Value
|
|
prevRoundPrivate = obj.Private
|
|
// Unfortunately our current state model represents dependencies only
|
|
// between static [addrs.ConfigResource] and loses specific instance
|
|
// information, so we must conservatively assume that all matching
|
|
// instances are dependencies. This loses the precision we get from
|
|
// dynamic analysis of the configuration, but it's the best we can
|
|
// do without switching to an updated model of state.
|
|
for _, configAddr := range prevRoundState.Dependencies {
|
|
for instAddr := range p.planCtx.prevRoundState.InstancesMatchingConfigResource(configAddr) {
|
|
ret.Dependencies.Add(instAddr.CurrentObject())
|
|
}
|
|
}
|
|
} else {
|
|
// TODO: Ask the planning oracle whether there are any "moved" blocks
|
|
// that ultimately end up at inst.Addr (possibly through a chain of
|
|
// multiple moves) and check the source instance address of each
|
|
// one in turn in case we find an as-yet-unclaimed resource instance
|
|
// that wants to be rebound to the address in inst.Addr.
|
|
// (Note that by handling moved blocks at _this_ point we could
|
|
// potentially have the eval system allow dynamic instance keys etc,
|
|
// which the original runtime can't do because it always deals with
|
|
// moved blocks as a preprocessing step before doing other work.)
|
|
prevRoundVal = cty.NullVal(schema.Block.ImpliedType())
|
|
}
|
|
|
|
// TODO: If inst.IgnoreChangesPaths has any entries then we need to
|
|
// transform effectiveConfigVal so that any paths specified in there are
|
|
// forced to match the corresponding value from prevRoundVal, if any.
|
|
effectiveConfigVal := inst.ConfigVal
|
|
|
|
// TODO: Call resourceType.RefreshObject, update the "refreshed state",
|
|
// and reassign this refreshedVal to the refreshed result.
|
|
refreshedVal := prevRoundVal
|
|
refreshedPrivate := prevRoundPrivate
|
|
|
|
// TODO: ProviderMeta is a rarely-used feature that only really makes
|
|
// sense when the module and provider are both written by the same
|
|
// party and the module author is using the provider as a way to
|
|
// transport module usage telemetry. We should decide whether we want
|
|
// to keep supporting that, and if so design a way for the relevant
|
|
// meta value to get from the evaluator into here.
|
|
providerMetaValue := cty.NilVal
|
|
|
|
planResp, planDiags := resourceType.PlanChanges(ctx, &resources.ManagedResourcePlanRequest{
|
|
Current: resources.ValueWithPrivate{
|
|
Value: refreshedVal,
|
|
Private: refreshedPrivate,
|
|
},
|
|
DesiredValue: effectiveConfigVal,
|
|
ProviderMetaValue: providerMetaValue,
|
|
}, ret.Addr)
|
|
diags = diags.Append(planDiags)
|
|
if planDiags.HasErrors() {
|
|
return ret, diags
|
|
}
|
|
|
|
// TODO: Check for resp.Deferred once we've updated package providers to
|
|
// include it. If that's set then the _provider_ is telling us we must
|
|
// defer planning any action for this resource instance. We'd still
|
|
// return the planned new state as a placeholder for downstream planning in
|
|
// that case, but we would need to mark it as deferred and _not_ record a
|
|
// proposed change for it.
|
|
|
|
if eq, _ := planResp.Planned.Value.Equals(refreshedVal).Unmark(); !eq.IsKnown() || eq.True() {
|
|
// There is no change to make, so we'll return early without actually
|
|
// recording any change. In this case our resource instance will be
|
|
// included in the execution graph only if some other resource instance
|
|
// depends on it, and even then only as a "read prior state" operation
|
|
// and no actual changes, so we'll set our placeholder to be the
|
|
// refreshed value to match what the prior state would contain during
|
|
// apply.
|
|
ret.PlaceholderValue = refreshedVal
|
|
ret.PlannedChange = nil
|
|
return ret, diags
|
|
}
|
|
|
|
plannedAction := plans.Update
|
|
if prevRoundState == nil {
|
|
plannedAction = plans.Create
|
|
} else if len(planResp.RequiresReplace) != 0 {
|
|
// For "replace" actions the execution graph will include two separate
|
|
// plan and apply operations, where one handles deletion and the other
|
|
// handles creation. There is therefore an implicit third intermediate
|
|
// state between those two, but in our plan model we have a convention
|
|
// to model it as if it were just a direct transition from the old
|
|
// object to the new object.
|
|
//
|
|
// Our current planResp.Planned.Value describes the situation as if
|
|
// we were performing an in-place update though, so we need to now
|
|
// ask the provider to plan each of the parts separately so that we
|
|
// can match how the apply engine will ask the provider these questions.
|
|
createPlanResp, planDiags := resourceType.PlanChanges(ctx, &resources.ManagedResourcePlanRequest{
|
|
// "Current" is intentionally not set here, because we're asking
|
|
// for a plan to create a new object matching the configuration.
|
|
DesiredValue: effectiveConfigVal,
|
|
ProviderMetaValue: providerMetaValue,
|
|
}, ret.Addr)
|
|
diags = diags.Append(planDiags)
|
|
if planDiags.HasErrors() {
|
|
return ret, diags
|
|
}
|
|
deletePlanResp, planDiags := resourceType.PlanChanges(ctx, &resources.ManagedResourcePlanRequest{
|
|
Current: resources.ValueWithPrivate{
|
|
Value: refreshedVal,
|
|
Private: refreshedPrivate,
|
|
},
|
|
// DesiredValue is intentionally not set here, because we're asking
|
|
// asking for a plan to just destroy what currently exists.
|
|
ProviderMetaValue: providerMetaValue,
|
|
}, ret.Addr)
|
|
diags = diags.Append(planDiags)
|
|
if planDiags.HasErrors() {
|
|
return ret, diags
|
|
}
|
|
// Now we'll update the original plan response with these newly-chosen
|
|
// before/after values, to match what the rest of the system expects.
|
|
planResp.Current = deletePlanResp.Current
|
|
planResp.DesiredValue = createPlanResp.DesiredValue
|
|
planResp.Planned = createPlanResp.Planned
|
|
|
|
// We'll select a reasonable initial planned action here but this
|
|
// might be overridden later once we propagate ordering constraints
|
|
// through the dependency graph.
|
|
if inst.CreateBeforeDestroy {
|
|
plannedAction = plans.CreateThenDelete
|
|
} else {
|
|
plannedAction = plans.DeleteThenCreate
|
|
}
|
|
}
|
|
// (a "desired" object cannot have a Delete action; we handle those cases
|
|
// in planOrphanManagedResourceInstance and planDeposedManagedResourceInstanceObject below.)
|
|
ret.PlannedChange = &plans.ResourceInstanceChange{
|
|
Addr: inst.Addr,
|
|
PrevRunAddr: inst.Addr, // TODO: If we add "moved" support above then this must record the original address
|
|
ProviderAddr: addrs.AbsProviderConfig{
|
|
// FIXME: This is a lossy shim to the old-style provider instance
|
|
// address representation, since our old models aren't yet updated
|
|
// to support the modern one. It cannot handle a provider config
|
|
// inside a module call that uses count or for_each.
|
|
Module: (*inst.ProviderInstance).Config.Module.Module(),
|
|
Provider: (*inst.ProviderInstance).Config.Config.Provider,
|
|
Alias: (*inst.ProviderInstance).Config.Config.Alias,
|
|
},
|
|
RequiredReplace: cty.NewPathSet(planResp.RequiresReplace...),
|
|
Private: planResp.Planned.Private,
|
|
Change: plans.Change{
|
|
Action: plannedAction,
|
|
Before: planResp.Current.Value,
|
|
After: planResp.Planned.Value,
|
|
},
|
|
|
|
// TODO: ActionReason, but need to figure out how to get the information
|
|
// we'd need for that into here since most of the reasons are
|
|
// configuration-related and so would need to be driven by stuff in
|
|
// [eval.DesiredResourceInstance].
|
|
}
|
|
ret.ProviderInst = *inst.ProviderInstance
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
func (p *planGlue) planOrphanManagedResourceInstance(
|
|
ctx context.Context,
|
|
addr addrs.AbsResourceInstance,
|
|
stateSrc *states.ResourceInstanceObjectFullSrc,
|
|
) (*resourceInstanceObject, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
// TODO: This currently has a lot of inline logic that's quite similar to
|
|
// what's in [planGlue.planDesiredManagedResourceInstance]. Once we're
|
|
// satisfied that this set of methods is feature-complete we should consider
|
|
// how to factor out as much of this logic as possible into shared functions
|
|
// so that this'll be easier to maintain in future as requirements change.
|
|
|
|
ret := &resourceInstanceObject{
|
|
Addr: addr.CurrentObject(),
|
|
Dependencies: addrs.MakeSet[addrs.AbsResourceInstanceObject](),
|
|
Provider: stateSrc.ProviderInstanceAddr.Config.Config.Provider,
|
|
|
|
// Orphan objects are always planned for deletion, so we can assume
|
|
// the result will be always be some kind of null.
|
|
PlaceholderValue: cty.NullVal(cty.DynamicPseudoType),
|
|
|
|
// NOTE: PlannedChange remains nil until we actually produce a plan,
|
|
// so early returns with errors are not guaranteed to have a valid
|
|
// change object. Evaluation falls back on using PlaceholderValue
|
|
// when no planned change is present.
|
|
}
|
|
// TODO: Populate ret.Dependencies based on the dependencies in the state,
|
|
// but to do that we'll need to correlate the [addrs.ConfigResource]-based
|
|
// dependencies with the actual resource instance objects in the prior state
|
|
// to get a comprehensive set of everything we ought to depend on.
|
|
|
|
// TODO: Ask the planning oracle whether there are any "moved" blocks
|
|
// that begin at inst.Addr, and if so check whether the chain of moves
|
|
// starting there will end up at a currently-unbound resource instance
|
|
// address. If so, we should do nothing here because
|
|
// [planGlue.planOrphanManagedResourceInstance] for that target address
|
|
// should notice the opposite end of the same chain of moves and so
|
|
// handle it as an object that is in both the prior and desired state,
|
|
// albeit with different addresses in each.
|
|
|
|
// FIXME: Currently this fails if the only mention of a particular provider
|
|
// instance is in the state, because this function relies on provider
|
|
// config information from the evaluator and thus only from the config.
|
|
// If you get the error about the provider not being able to initialize
|
|
// then you might currently need to add an explicit empty provider config
|
|
// block for the provider, if you were testing with a provider like
|
|
// hashicorp/null where an explicit configuration is not normally required.
|
|
//
|
|
// There's another FIXME comment further down the callstack beneath this
|
|
// function identifying the main location of the problem.
|
|
providerAddr := stateSrc.ProviderInstanceAddr.Config.Config.Provider
|
|
providerClient, moreDiags := p.providerClient(ctx, stateSrc.ProviderInstanceAddr)
|
|
if providerClient == nil {
|
|
moreDiags = moreDiags.Append(tfdiags.AttributeValue(
|
|
tfdiags.Error,
|
|
"Provider instance not available",
|
|
fmt.Sprintf("Cannot plan %s because its associated provider instance %s cannot initialize.", addr, stateSrc.ProviderInstanceAddr),
|
|
nil,
|
|
))
|
|
}
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
return ret, diags
|
|
}
|
|
|
|
resourceType := resources.NewManagedResourceType(providerAddr, addr.Resource.Resource.Type, providerClient)
|
|
schema, schemaDiags := resourceType.LoadSchema(ctx)
|
|
if schemaDiags.HasErrors() {
|
|
// We don't return the schema-loading diagnostics directly here because
|
|
// they should have already been returned by earlier code, but we do
|
|
// return a more specific error to make it clear that this specific
|
|
// resource instance was unplannable because of the problem.
|
|
diags = diags.Append(tfdiags.AttributeValue(
|
|
tfdiags.Error,
|
|
"Resource type schema unavailable",
|
|
fmt.Sprintf(
|
|
"Cannot plan %s because provider %s failed to return the schema for its resource type %q.",
|
|
addr, providerAddr, addr.Resource.Resource.Type,
|
|
),
|
|
nil, // this error belongs to the whole resource config
|
|
))
|
|
return ret, diags
|
|
}
|
|
|
|
var prevRoundVal cty.Value
|
|
var prevRoundPrivate []byte
|
|
prevRoundState, err := states.DecodeResourceInstanceObjectFull(stateSrc, schema.Block.ImpliedType())
|
|
if err != nil {
|
|
diags = diags.Append(tfdiags.AttributeValue(
|
|
tfdiags.Error,
|
|
"Invalid prior state for resource instance",
|
|
fmt.Sprintf(
|
|
"Cannot decode the most recent state snapshot for %s: %s.\n\nIs the selected version of %s incompatible with the provider that most recently changed this object?",
|
|
addr, tfdiags.FormatError(err), providerAddr,
|
|
),
|
|
nil, // this error belongs to the whole resource config
|
|
))
|
|
return ret, diags
|
|
}
|
|
prevRoundVal = prevRoundState.Value
|
|
prevRoundPrivate = prevRoundState.Private
|
|
// Unfortunately our current state model represents dependencies only
|
|
// between static [addrs.ConfigResource] and loses specific instance
|
|
// information, so we must conservatively assume that all matching
|
|
// instances are dependencies. This loses the precision we get from
|
|
// dynamic analysis of the configuration, but it's the best we can
|
|
// do without switching to an updated model of state.
|
|
for _, configAddr := range prevRoundState.Dependencies {
|
|
for instAddr := range p.planCtx.prevRoundState.InstancesMatchingConfigResource(configAddr) {
|
|
ret.Dependencies.Add(instAddr.CurrentObject())
|
|
}
|
|
}
|
|
|
|
// TODO: Call providerClient.ReadResource and update the "refreshed state"
|
|
// and reassign this refreshedVal to the refreshed result.
|
|
refreshedVal := prevRoundVal
|
|
refreshedPrivate := prevRoundPrivate
|
|
|
|
if refreshedVal.IsNull() {
|
|
// The orphan object seems to have already been deleted outside of
|
|
// OpenTofu, so we've got nothing more to do here.
|
|
ret.PlaceholderValue = refreshedVal
|
|
return ret, diags
|
|
}
|
|
|
|
planResp, planDiags := resourceType.PlanChanges(ctx, &resources.ManagedResourcePlanRequest{
|
|
Current: resources.ValueWithPrivate{
|
|
Value: refreshedVal,
|
|
Private: refreshedPrivate,
|
|
},
|
|
DesiredValue: cty.NilVal, // we want to destroy this object
|
|
|
|
// TODO: ProviderMeta is a rarely-used feature that only really makes
|
|
// sense when the module and provider are both written by the same
|
|
// party and the module author is using the provider as a way to
|
|
// transport module usage telemetry. We should decide whether we want
|
|
// to keep supporting that, and if so design a way for the relevant
|
|
// meta value to get from the evaluator into here.
|
|
ProviderMetaValue: cty.NilVal,
|
|
}, addr.CurrentObject())
|
|
diags = diags.Append(planDiags)
|
|
if planDiags.HasErrors() {
|
|
return ret, diags
|
|
}
|
|
|
|
ret.PlannedChange = &plans.ResourceInstanceChange{
|
|
Addr: addr,
|
|
PrevRunAddr: addr,
|
|
ProviderAddr: addrs.AbsProviderConfig{
|
|
// FIXME: This is a lossy shim to the old-style provider instance
|
|
// address representation, since our old models aren't yet updated
|
|
// to support the modern one. It cannot handle a provider config
|
|
// inside a module call that uses count or for_each.
|
|
Module: prevRoundState.ProviderInstanceAddr.Config.Module.Module(),
|
|
Provider: prevRoundState.ProviderInstanceAddr.Config.Config.Provider,
|
|
Alias: prevRoundState.ProviderInstanceAddr.Config.Config.Alias,
|
|
},
|
|
RequiredReplace: cty.NewPathSet(planResp.RequiresReplace...),
|
|
Private: planResp.Planned.Private,
|
|
Change: plans.Change{
|
|
Action: plans.Delete,
|
|
Before: refreshedVal,
|
|
After: planResp.Planned.Value,
|
|
},
|
|
|
|
// TODO: ActionReason, but need to figure out how to get the information
|
|
// we'd need for that into here. For example, to report that the
|
|
// instance address is no longer in the configuration we need to be
|
|
// able to refer to the configuration in here. Or maybe our caller
|
|
// should just pass in a reason as an additonal argument to this
|
|
// function, since it presumably already knows how it concluded that
|
|
// this address is "orphaned".
|
|
}
|
|
ret.ProviderInst = prevRoundState.ProviderInstanceAddr
|
|
return ret, diags
|
|
}
|
|
|
|
func (p *planGlue) planDeposedManagedResourceInstanceObject(
|
|
ctx context.Context,
|
|
addr addrs.AbsResourceInstance,
|
|
deposedKey states.DeposedKey,
|
|
state *states.ResourceInstanceObjectFullSrc,
|
|
) (*resourceInstanceObject, tfdiags.Diagnostics) {
|
|
// TODO: Implement
|
|
panic("unimplemented")
|
|
}
|