// 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" "github.com/zclconf/go-cty/cty" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/engine/internal/exec" "github.com/opentofu/opentofu/internal/engine/internal/execgraph" "github.com/opentofu/opentofu/internal/lang/eval" "github.com/opentofu/opentofu/internal/plans" "github.com/opentofu/opentofu/internal/states" ) //////////////////////////////////////////////////////////////////////////////// // This file contains methods of [execGraphBuilder] that are related to the // parts of an execution graph that deal with resource instances of mode // [addrs.ManagedResourceMode] in particular. //////////////////////////////////////////////////////////////////////////////// // ManagedResourceSubgraph adds graph nodes needed to apply changes for a // managed resource instance, and returns various items needed to describe // its relationships with other resource instance and provider instance // subgraphs. func (b *execGraphBuilder) ManagedResourceInstanceSubgraph( plannedChange *plans.ResourceInstanceChange, effectiveReplaceOrder resourceInstanceReplaceOrder, providerClientRef execgraph.ResultRef[*exec.ProviderClient], ) ( valueRef, deletionRef execgraph.ResourceInstanceResultRef, // reference to the final new value and, if addDeleteDep is not nil, the deletion result addConfigDep, addDeleteDep func(execgraph.AnyResultRef), // callbacks to register explicit dependencies, or nil when not relevant ) { b.mu.Lock() defer b.mu.Unlock() // Before we go any further we'll just make sure what we've been given // is sensible, so that the remaining code can assume the following // about the given change. Any panics in the following suggest that there's // a bug in the caller, unless we're intentionally changing the rules // for what the different action types represent. if plannedChange.DeposedKey != states.NotDeposed && plannedChange.Action != plans.Delete { // The only sensible thing to do with a deposed object is to delete it. panic(fmt.Sprintf("invalid action %s for %s deposed object %s", plannedChange.Action, plannedChange.PrevRunAddr, plannedChange.DeposedKey)) } if plannedChange.Action == plans.Create && !plannedChange.Before.IsNull() { panic(fmt.Sprintf("for %s has action %s but non-null prior value", plannedChange.Addr, plannedChange.Action)) } if (plannedChange.Action == plans.Delete || plannedChange.Action == plans.Forget) && !plannedChange.After.IsNull() { panic(fmt.Sprintf("change for %s has action %s but non-null planned new value", plannedChange.PrevRunAddr, plannedChange.Action)) } if plannedChange.Action != plans.Create && plannedChange.Action != plans.Delete && plannedChange.Action != plans.Forget && (plannedChange.Before.IsNull() || plannedChange.After.IsNull()) { panic(fmt.Sprintf("change for %s has action %s but does not have both a before and after value", plannedChange.PrevRunAddr, plannedChange.Action)) } changeAction := plannedChange.Action if changeAction.IsReplace() { // The effective replace order finalizes which of the two replace // actions we will actually use. changeAction = effectiveReplaceOrder.ChangeAction() } // The shape of execution subgraph we generate here varies depending on // which change action was planned. switch changeAction { case plans.Create: valueRef, addConfigDep = b.managedResourceInstanceSubgraphCreate(plannedChange, providerClientRef) case plans.Update: valueRef, addConfigDep = b.managedResourceInstanceSubgraphUpdate(plannedChange, providerClientRef) case plans.Delete: deletionRef, addDeleteDep = b.managedResourceInstanceSubgraphDelete(plannedChange, providerClientRef) case plans.Forget: valueRef = b.managedResourceInstanceSubgraphForget(plannedChange, providerClientRef) case plans.DeleteThenCreate, plans.ForgetThenCreate: valueRef, deletionRef, addConfigDep, addDeleteDep = b.managedResourceInstanceSubgraphDeleteOrForgetThenCreate(plannedChange, providerClientRef) case plans.CreateThenDelete: valueRef, deletionRef, addConfigDep, addDeleteDep = b.managedResourceInstanceSubgraphCreateThenDelete(plannedChange, providerClientRef) default: // FIXME: We need to handle plans.NoOp too because that can occur if // the configuration hasn't changed but the object will move to a // new resource instance address during the apply phase. // We should not get here: the cases above should cover every action // that [planGlue.planDesiredManagedResourceInstance] can possibly // produce. panic(fmt.Sprintf("unsupported change action %s for %s", plannedChange.Action, plannedChange.Addr)) } return valueRef, deletionRef, addConfigDep, addDeleteDep } func (b *execGraphBuilder) managedResourceInstanceSubgraphCreate( plannedChange *plans.ResourceInstanceChange, providerClientRef execgraph.ResultRef[*exec.ProviderClient], ) (execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef)) { instAddrRef, _ := b.managedResourceInstanceChangeAddrAndPriorStateRefs(plannedChange) plannedValRef := b.lower.ConstantValue(plannedChange.After) waitFor, addConfigDep := b.lower.MutableWaiter() desiredInstRef := b.lower.ResourceInstanceDesired(instAddrRef, waitFor) return b.managedResourceInstanceSubgraphPlanAndApply( desiredInstRef, execgraph.NilResultRef[*exec.ResourceInstanceObject](), plannedValRef, providerClientRef, ), addConfigDep } func (b *execGraphBuilder) managedResourceInstanceSubgraphUpdate( plannedChange *plans.ResourceInstanceChange, providerClientRef execgraph.ResultRef[*exec.ProviderClient], ) (execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef)) { instAddrRef, priorStateRef := b.managedResourceInstanceChangeAddrAndPriorStateRefs(plannedChange) plannedValRef := b.lower.ConstantValue(plannedChange.After) waitFor, addConfigDep := b.lower.MutableWaiter() desiredInstRef := b.lower.ResourceInstanceDesired(instAddrRef, waitFor) return b.managedResourceInstanceSubgraphPlanAndApply( desiredInstRef, priorStateRef, plannedValRef, providerClientRef, ), addConfigDep } // managedResourceInstanceSubgraphPlanAndApply deals with the simple case // of "create a final plan and then apply it" that is shared between "create" // and "update", but not for deleting or for the more complicated ones involving // multiple primitive actions that need to be carefully coordinated with each // other. func (b *execGraphBuilder) managedResourceInstanceSubgraphPlanAndApply( desiredInstRef execgraph.ResultRef[*eval.DesiredResourceInstance], priorStateRef execgraph.ResourceInstanceResultRef, plannedValRef execgraph.ResultRef[cty.Value], providerClientRef execgraph.ResultRef[*exec.ProviderClient], ) execgraph.ResourceInstanceResultRef { finalPlanRef := b.lower.ManagedFinalPlan( desiredInstRef, priorStateRef, plannedValRef, providerClientRef, ) return b.lower.ManagedApply( finalPlanRef, execgraph.NilResultRef[*exec.ResourceInstanceObject](), providerClientRef, b.lower.Waiter(), // nothing to wait for: desiredInstRef should have the relevant dependencies itself ) } func (b *execGraphBuilder) managedResourceInstanceSubgraphDelete( plannedChange *plans.ResourceInstanceChange, providerClientRef execgraph.ResultRef[*exec.ProviderClient], ) (execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef)) { _, priorStateRef := b.managedResourceInstanceChangeAddrAndPriorStateRefs(plannedChange) plannedValRef := b.lower.ConstantValue(plannedChange.After) waitFor, addDeleteDep := b.lower.MutableWaiter() finalPlanRef := b.lower.ManagedFinalPlan( execgraph.NilResultRef[*eval.DesiredResourceInstance](), priorStateRef, plannedValRef, providerClientRef, ) return b.lower.ManagedApply( finalPlanRef, execgraph.NilResultRef[*exec.ResourceInstanceObject](), providerClientRef, waitFor, ), addDeleteDep } func (b *execGraphBuilder) managedResourceInstanceSubgraphForget( plannedChange *plans.ResourceInstanceChange, providerClientRef execgraph.ResultRef[*exec.ProviderClient], ) execgraph.ResourceInstanceResultRef { // TODO: Add a new execgraph opcode ManagedForget and use that here. panic("execgraph for Forget not yet implemented") } func (b *execGraphBuilder) managedResourceInstanceSubgraphDeleteOrForgetThenCreate( plannedChange *plans.ResourceInstanceChange, providerClientRef execgraph.ResultRef[*exec.ProviderClient], ) (execgraph.ResourceInstanceResultRef, execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef), func(execgraph.AnyResultRef)) { if plannedChange.Action == plans.ForgetThenCreate { // TODO: Implement this action too, which is similar but with the // "delete" let replaced with something like what // managedResourceInstanceSubgraphForget would generate. panic("execgraph for ForgetThenCreate not yet implemented") } desiredWaitFor, addConfigDep := b.lower.MutableWaiter() deleteWaitFor, addDeleteDep := b.lower.MutableWaiter() // This has much the same _effect_ as the separate delete and create // actions chained together, but we arrange the operations in such a // way that the delete leg can't start unless the desired state is // successfully evaluated. instAddrRef, priorStateRef := b.managedResourceInstanceChangeAddrAndPriorStateRefs(plannedChange) plannedValRef := b.lower.ConstantValue(plannedChange.After) desiredInstRef := b.lower.ResourceInstanceDesired(instAddrRef, desiredWaitFor) // We plan both the create and destroy parts of this process before we // make any real changes, to reduce the risk that we'll be left in a // partially-applied state where neither object exists. (Though of course // that's always possible, if the "create" step fails at apply.) createPlanRef := b.lower.ManagedFinalPlan( desiredInstRef, execgraph.NilResultRef[*exec.ResourceInstanceObject](), plannedValRef, providerClientRef, ) destroyPlanRef := b.lower.ManagedFinalPlan( execgraph.NilResultRef[*eval.DesiredResourceInstance](), priorStateRef, b.lower.ConstantValue(cty.NullVal( // TODO: is this okay or do we need to use the type constraint derived from the schema? // The two could differ for resource types that have cty.DynamicPseudoType // attributes, like in kubernetes_manifest from the hashicorp/kubernetes provider, // where here we'd capture the type of the current manifest instead of recording // that the manifest's type is unknown. However, we don't typically fuss too much // about the exact type of a null, so this is probably fine. plannedChange.After.Type(), )), providerClientRef, ) destroyResultRef := b.lower.ManagedApply( destroyPlanRef, execgraph.NilResultRef[*exec.ResourceInstanceObject](), providerClientRef, deleteWaitFor, ) createResultRef := b.lower.ManagedApply( createPlanRef, execgraph.NilResultRef[*exec.ResourceInstanceObject](), providerClientRef, b.lower.Waiter(destroyResultRef), ) return createResultRef, destroyResultRef, addConfigDep, addDeleteDep } func (b *execGraphBuilder) managedResourceInstanceSubgraphCreateThenDelete( plannedChange *plans.ResourceInstanceChange, providerClientRef execgraph.ResultRef[*exec.ProviderClient], ) (execgraph.ResourceInstanceResultRef, execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef), func(execgraph.AnyResultRef)) { desiredWaitFor, addConfigDep := b.lower.MutableWaiter() deleteWaitFor, addDeleteDep := b.lower.MutableWaiter() // This has much the same effect as the separate delete and create // actions chained together, but we arrange the operations in such a // way that we don't make any changes unless we can produce valid final // plans for both changes. instAddrRef, priorStateRef := b.managedResourceInstanceChangeAddrAndPriorStateRefs(plannedChange) plannedValRef := b.lower.ConstantValue(plannedChange.After) desiredInstRef := b.lower.ResourceInstanceDesired(instAddrRef, desiredWaitFor) // We plan both the create and destroy parts of this process before we // make any real changes, to reduce the risk that we'll be left in a // partially-applied state where we're left with a deposed object present // in the final state. createPlanRef := b.lower.ManagedFinalPlan( desiredInstRef, execgraph.NilResultRef[*exec.ResourceInstanceObject](), plannedValRef, providerClientRef, ) destroyPlanRef := b.lower.ManagedFinalPlan( execgraph.NilResultRef[*eval.DesiredResourceInstance](), priorStateRef, b.lower.ConstantValue(cty.NullVal( // TODO: is this okay or do we need to use the type constraint derived from the schema? // The two could differ for resource types that have cty.DynamicPseudoType // attributes, like in kubernetes_manifest from the hashicorp/kubernetes provider, // where here we'd capture the type of the current manifest instead of recording // that the manifest's type is unknown. However, we don't typically fuss too much // about the exact type of a null, so this is probably fine. plannedChange.After.Type(), )), providerClientRef, ) deposedObjRef := b.lower.ManagedDepose( priorStateRef, b.lower.Waiter(createPlanRef, destroyPlanRef), ) createResultRef := b.lower.ManagedApply( createPlanRef, deposedObjRef, // will be restored as current if creation completely fails providerClientRef, b.lower.Waiter(), ) // No other resource instances can depend on the value from the destroy // result, so if the destroy fails after the create succeeded then we can // proceed with applying any downstream changes that refer to what we // created and then we'll end with the deposed object still in the state and // error diagnostics explaining why destroying it didn't work. addDeleteDep(createResultRef) // delete must not begin until creation has succeeded deletionRef := b.lower.ManagedApply( destroyPlanRef, execgraph.NilResultRef[*exec.ResourceInstanceObject](), providerClientRef, deleteWaitFor, ) return createResultRef, deletionRef, addConfigDep, addDeleteDep } func (b *execGraphBuilder) managedResourceInstanceChangeAddrAndPriorStateRefs( plannedChange *plans.ResourceInstanceChange, ) ( newAddr execgraph.ResultRef[addrs.AbsResourceInstance], priorState execgraph.ResourceInstanceResultRef, ) { if plannedChange.Action == plans.Create { // For a create change there is no prior state at all, but we still // need the new instance address. newAddrRef := b.lower.ConstantResourceInstAddr(plannedChange.Addr) return newAddrRef, execgraph.NilResultRef[*exec.ResourceInstanceObject]() } if plannedChange.DeposedKey != states.NotDeposed { // We need to use a different operation to access deposed objects. prevAddrRef := b.lower.ConstantResourceInstAddr(plannedChange.PrevRunAddr) dkRef := b.lower.ConstantDeposedKey(plannedChange.DeposedKey) stateRef := b.lower.ManagedAlreadyDeposed(prevAddrRef, dkRef) return execgraph.NilResultRef[addrs.AbsResourceInstance](), stateRef } prevAddrRef := b.lower.ConstantResourceInstAddr(plannedChange.PrevRunAddr) priorStateRef := b.lower.ResourceInstancePrior(prevAddrRef) retAddrRef := prevAddrRef retStateRef := priorStateRef if !plannedChange.PrevRunAddr.Equal(plannedChange.Addr) { // If the address is changing then we'll also include the // "change address" operation so that the object will get rebound // to its new address before we do any other work. retAddrRef = b.lower.ConstantResourceInstAddr(plannedChange.Addr) retStateRef = b.lower.ManagedChangeAddr(retStateRef, retAddrRef) } return retAddrRef, retStateRef }