// 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 } 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") }