mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
241 lines
9.8 KiB
Go
241 lines
9.8 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/plans/objchange"
|
|
"github.com/opentofu/opentofu/internal/providers"
|
|
"github.com/opentofu/opentofu/internal/states"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
func (p *planGlue) planDesiredManagedResourceInstance(ctx context.Context, inst *eval.DesiredResourceInstance) (plannedVal cty.Value, diags tfdiags.Diagnostics) {
|
|
// Regardless of outcome we'll always report that we completed planning.
|
|
defer p.planCtx.reportResourceInstancePlanCompletion(inst.Addr)
|
|
|
|
// 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 plannedVal != cty.NilVal {
|
|
plannedVal = deferredVal(plannedVal)
|
|
}
|
|
}()
|
|
// 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.
|
|
}
|
|
|
|
evalCtx := p.oracle.EvalContext(ctx)
|
|
schema, schemaDiags := evalCtx.Providers.ResourceTypeSchema(ctx,
|
|
inst.Provider,
|
|
inst.Addr.Resource.Resource.Mode,
|
|
inst.Addr.Resource.Resource.Type,
|
|
)
|
|
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 cty.DynamicVal, diags
|
|
}
|
|
|
|
validateDiags := p.planCtx.providers.ValidateResourceConfig(ctx, inst.Provider, inst.Addr.Resource.Resource.Mode, inst.Addr.Resource.Resource.Type, inst.ConfigVal)
|
|
diags = diags.Append(validateDiags)
|
|
if diags.HasErrors() {
|
|
return cty.DynamicVal, diags
|
|
}
|
|
|
|
var prevRoundVal cty.Value
|
|
var prevRoundPrivate []byte
|
|
prevRoundState := p.planCtx.prevRoundState.ResourceInstance(inst.Addr)
|
|
if prevRoundState != nil && prevRoundState.Current != nil {
|
|
obj, err := prevRoundState.Current.Decode(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 cty.DynamicVal, diags
|
|
}
|
|
prevRoundVal = obj.Value
|
|
prevRoundPrivate = obj.Private
|
|
} 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())
|
|
}
|
|
|
|
proposedNewVal := p.resourceInstancePlaceholderValue(ctx,
|
|
inst.Provider,
|
|
inst.Addr.Resource.Resource.Mode,
|
|
inst.Addr.Resource.Resource.Type,
|
|
prevRoundVal,
|
|
inst.ConfigVal,
|
|
)
|
|
|
|
if inst.ProviderInstance == nil {
|
|
// If we don't even know which provider instance we're supposed to be
|
|
// talking to then we'll just return a placeholder value, because
|
|
// we don't have any way to generate a speculative plan.
|
|
return proposedNewVal, 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 proposedNewVal, diags
|
|
}
|
|
|
|
// 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 providerClient.ReadResource and update the "refreshed state"
|
|
// and reassign this refreshedVal to the refreshed result.
|
|
refreshedVal := prevRoundVal
|
|
|
|
// As long as we have a provider instance we should be able to ask the
|
|
// provider to plan _something_. If this is a placeholder for zero or more
|
|
// instances of a resource whose expansion isn't yet known then we're asking
|
|
// the provider to produce a speculative plan for all of them at once,
|
|
// so we can catch whatever subset of problems are already obvious across
|
|
// all of the potential resource instances.
|
|
planResp := providerClient.PlanResourceChange(ctx, providers.PlanResourceChangeRequest{
|
|
TypeName: inst.Addr.Resource.Resource.Type,
|
|
PriorState: refreshedVal,
|
|
ProposedNewState: proposedNewVal,
|
|
Config: effectiveConfigVal,
|
|
PriorPrivate: prevRoundPrivate,
|
|
// TODO: ProviderMeta
|
|
})
|
|
for _, err := range objchange.AssertPlanValid(schema.Block, refreshedVal, effectiveConfigVal, planResp.PlannedState) {
|
|
// TODO: If resp.LegacyTypeSystem is set then we should generate
|
|
// warnings in the log but continue anyway, like the original
|
|
// runtime does.
|
|
planResp.Diagnostics = planResp.Diagnostics.Append(tfdiags.AttributeValue(
|
|
tfdiags.Error,
|
|
"Provider produced invalid plan",
|
|
// TODO: Bring over the full version of this error case from the
|
|
// original runtime.
|
|
fmt.Sprintf("Invalid planned new value: %s.", tfdiags.FormatError(err)),
|
|
nil,
|
|
))
|
|
}
|
|
diags = diags.Append(planResp.Diagnostics)
|
|
if planResp.Diagnostics.HasErrors() {
|
|
return proposedNewVal, 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.
|
|
|
|
plannedAction := plans.Update
|
|
if prevRoundState == nil {
|
|
plannedAction = plans.Create
|
|
} else if len(planResp.RequiresReplace) != 0 {
|
|
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.)
|
|
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: Old models are using the not-quite-correct provider address types, so we can't populate this properly
|
|
RequiredReplace: cty.NewPathSet(planResp.RequiresReplace...),
|
|
Private: planResp.PlannedPrivate,
|
|
Change: plans.Change{
|
|
Action: plannedAction,
|
|
Before: refreshedVal,
|
|
After: planResp.PlannedState,
|
|
},
|
|
|
|
// 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].
|
|
}
|
|
plannedChangeSrc, err := plannedChange.Encode(schema.Block.ImpliedType())
|
|
if err != nil {
|
|
// TODO: Make a proper error diagnostic for this, like the original
|
|
// runtime does.
|
|
diags = diags.Append(err)
|
|
return planResp.PlannedState, diags
|
|
}
|
|
p.planCtx.plannedChanges.AppendResourceInstanceChange(plannedChangeSrc)
|
|
|
|
// Our result value for ongoing downstream planning is the planned new state.
|
|
return planResp.PlannedState, diags
|
|
}
|
|
|
|
func (p *planGlue) planOrphanManagedResourceInstance(ctx context.Context, addr addrs.AbsResourceInstance, state *states.ResourceInstance) tfdiags.Diagnostics {
|
|
// Regardless of outcome we'll always report that we completed planning.
|
|
defer p.planCtx.reportResourceInstancePlanCompletion(addr)
|
|
|
|
// TODO: Implement
|
|
panic("unimplemented")
|
|
}
|
|
|
|
func (p *planGlue) planDeposedManagedResourceInstanceObject(ctx context.Context, addr addrs.AbsResourceInstance, deposedKey states.DeposedKey, state *states.ResourceInstance) tfdiags.Diagnostics {
|
|
// Regardless of outcome we'll always report that we completed planning.
|
|
defer p.planCtx.reportResourceInstanceDeposedPlanCompletion(addr, deposedKey)
|
|
|
|
// TODO: Implement
|
|
panic("unimplemented")
|
|
}
|