mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
Signed-off-by: Ilia Gogotchuri <ilia.gogotchuri0@gmail.com> Co-authored-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
311 lines
11 KiB
Go
311 lines
11 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 tofu
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/configs"
|
|
"github.com/opentofu/opentofu/internal/dag"
|
|
"github.com/opentofu/opentofu/internal/plans"
|
|
"github.com/opentofu/opentofu/internal/refactoring"
|
|
"github.com/opentofu/opentofu/internal/states"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
// DiffTransformer is a GraphTransformer that adds graph nodes representing
|
|
// each of the resource changes described in the given Changes object.
|
|
type DiffTransformer struct {
|
|
Concrete ConcreteResourceInstanceNodeFunc
|
|
State *states.State
|
|
Changes *plans.Changes
|
|
Config *configs.Config
|
|
}
|
|
|
|
// return true if the given resource instance has either Preconditions or
|
|
// Postconditions defined in the configuration.
|
|
func (t *DiffTransformer) hasConfigConditions(addr addrs.AbsResourceInstance) bool {
|
|
// unit tests may have no config
|
|
if t.Config == nil {
|
|
return false
|
|
}
|
|
|
|
cfg := t.Config.DescendentForInstance(addr.Module)
|
|
if cfg == nil {
|
|
return false
|
|
}
|
|
|
|
res := cfg.Module.ResourceByAddr(addr.ConfigResource().Resource)
|
|
if res == nil {
|
|
return false
|
|
}
|
|
|
|
return len(res.Preconditions) > 0 || len(res.Postconditions) > 0
|
|
}
|
|
|
|
func (t *DiffTransformer) Transform(_ context.Context, g *Graph) error {
|
|
if t.Changes == nil || len(t.Changes.Resources) == 0 {
|
|
// Nothing to do!
|
|
return nil
|
|
}
|
|
|
|
// Go through all the modules in the diff.
|
|
log.Printf("[TRACE] DiffTransformer starting")
|
|
|
|
var diags tfdiags.Diagnostics
|
|
state := t.State
|
|
changes := t.Changes
|
|
|
|
// DiffTransformer creates resource _instance_ nodes. If there are any
|
|
// whole-resource nodes already in the graph, we must ensure that they
|
|
// get evaluated before any of the corresponding instances by creating
|
|
// dependency edges, so we'll do some prep work here to ensure we'll only
|
|
// create connections to nodes that existed before we started here.
|
|
resourceNodes := map[string][]GraphNodeConfigResource{}
|
|
for _, node := range g.Vertices() {
|
|
rn, ok := node.(GraphNodeConfigResource)
|
|
if !ok {
|
|
continue
|
|
}
|
|
// We ignore any instances that _also_ implement
|
|
// GraphNodeResourceInstance, since in the unlikely event that they
|
|
// do exist we'd probably end up creating cycles by connecting them.
|
|
if _, ok := node.(GraphNodeResourceInstance); ok {
|
|
continue
|
|
}
|
|
|
|
addr := rn.ResourceAddr().String()
|
|
resourceNodes[addr] = append(resourceNodes[addr], rn)
|
|
}
|
|
|
|
for _, rc := range changes.Resources {
|
|
addr := rc.Addr
|
|
dk := rc.DeposedKey
|
|
|
|
log.Printf("[TRACE] DiffTransformer: found %s change for %s %s", rc.Action, addr, dk)
|
|
|
|
// Depending on the action we'll need some different combinations of
|
|
// nodes, because destroying uses a special node type separate from
|
|
// other actions.
|
|
var update, delete, forget, open, createBeforeDestroy bool
|
|
switch rc.Action {
|
|
case plans.NoOp:
|
|
// For a no-op change we don't take any action but we still
|
|
// run any condition checks associated with the object, to
|
|
// make sure that they still hold when considering the
|
|
// results of other changes.
|
|
update = t.hasConfigConditions(addr)
|
|
case plans.Delete:
|
|
delete = true
|
|
case plans.Forget:
|
|
forget = true
|
|
case plans.ForgetThenCreate:
|
|
update = true
|
|
forget = true
|
|
case plans.DeleteThenCreate, plans.CreateThenDelete:
|
|
update = true
|
|
delete = true
|
|
createBeforeDestroy = (rc.Action == plans.CreateThenDelete)
|
|
case plans.Open:
|
|
open = true
|
|
default:
|
|
update = true
|
|
}
|
|
|
|
// A deposed instance may only have a change of Delete, Forget or NoOp.
|
|
// A NoOp can happen if the provider shows it no longer exists during
|
|
// the most recent ReadResource operation.
|
|
if dk != states.NotDeposed && rc.Action != plans.Delete && rc.Action != plans.NoOp && rc.Action != plans.Forget {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Invalid planned change for deposed object",
|
|
fmt.Sprintf("The plan contains a non-removal change for %s deposed object %s. The only valid actions for a deposed object is to destroy it or forget it, so this is a bug in OpenTofu.", addr, dk),
|
|
))
|
|
continue
|
|
}
|
|
|
|
// If we're going to do a create_before_destroy Replace operation then
|
|
// we need to allocate a DeposedKey to use to retain the
|
|
// not-yet-destroyed prior object, so that the delete node can destroy
|
|
// _that_ rather than the newly-created node, which will be current
|
|
// by the time the delete node is visited.
|
|
if update && delete && createBeforeDestroy {
|
|
// In this case, variable dk will be the _pre-assigned_ DeposedKey
|
|
// that must be used if the update graph node deposes the current
|
|
// instance, which will then align with the same key we pass
|
|
// into the destroy node to ensure we destroy exactly the deposed
|
|
// object we expect.
|
|
if state != nil {
|
|
ris := state.ResourceInstance(addr)
|
|
if ris == nil {
|
|
// Should never happen, since we don't plan to replace an
|
|
// instance that doesn't exist yet.
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Invalid planned change",
|
|
fmt.Sprintf("The plan contains a replace change for %s, which doesn't exist yet. This is a bug in OpenTofu.", addr),
|
|
))
|
|
continue
|
|
}
|
|
|
|
// Allocating a deposed key separately from using it can be racy
|
|
// in general, but we assume here that nothing except the apply
|
|
// node we instantiate below will actually make new deposed objects
|
|
// in practice, and so the set of already-used keys will not change
|
|
// between now and then.
|
|
dk = ris.FindUnusedDeposedKey()
|
|
} else {
|
|
// If we have no state at all yet then we can use _any_
|
|
// DeposedKey.
|
|
dk = states.NewDeposedKey()
|
|
}
|
|
}
|
|
|
|
if open {
|
|
// Ephemeral resources are always opened, even if the value is not used.
|
|
// This means that to ensure that the ephemeral resource instance node is created and connected
|
|
// to the resource node, we create it here.
|
|
// If we do not have this logic here, ephemeral resource instances that are not used by any other resource
|
|
// or output would be pruned away during the unused nodes pruning step, and thus not opened.
|
|
abstract := NewNodeAbstractResourceInstance(addr)
|
|
var node dag.Vertex = abstract
|
|
if t.Concrete != nil {
|
|
node = t.Concrete(abstract)
|
|
}
|
|
|
|
g.Add(node)
|
|
|
|
resourceContaining := addr.ContainingResource()
|
|
// strip the instance key from the module instance
|
|
resourceAddress := addrs.AbsResource{
|
|
Module: resourceContaining.Module.Module().UnkeyedInstanceShim(),
|
|
Resource: resourceContaining.Resource,
|
|
}.String()
|
|
|
|
// Ensure that the ephemeral resource instance node connects to
|
|
// the resource node. This is needed to ensure that the ephemeral
|
|
// expansion node will not get pruned due to having no connections
|
|
for _, resourceNode := range resourceNodes[resourceAddress] {
|
|
g.Connect(dag.BasicEdge(node, resourceNode))
|
|
}
|
|
}
|
|
|
|
if update {
|
|
// All actions except destroying the node type chosen by t.Concrete
|
|
abstract := NewNodeAbstractResourceInstance(addr)
|
|
var node dag.Vertex = abstract
|
|
if t.Concrete != nil {
|
|
node = t.Concrete(abstract)
|
|
}
|
|
|
|
if createBeforeDestroy {
|
|
// We'll attach our pre-allocated DeposedKey to the node if
|
|
// it supports that. NodeApplyableResourceInstance is the
|
|
// specific concrete node type we are looking for here really,
|
|
// since that's the only node type that might depose objects.
|
|
if dn, ok := node.(GraphNodeDeposer); ok {
|
|
dn.SetPreallocatedDeposedKey(dk)
|
|
}
|
|
|
|
// We need to set CBD to the node here, otherwise if CBD flag was caused
|
|
// by the CBD descendant of the node (and not the config) and this is the sole node being updated
|
|
// in the current apply operation, we will lose the CBD flag in the state file and cause the "cycle" error down the line.
|
|
// For more details, see the issue https://github.com/opentofu/opentofu/issues/2398
|
|
if cn, ok := node.(GraphNodeDestroyerCBD); ok {
|
|
log.Printf("[TRACE] DiffTransformer: %s implements GraphNodeDestroyerCBD, setting CBD to true", addr)
|
|
// Setting CBD to true
|
|
// Error handling here is just for future-proofing, since none of the GraphNodeDestroyerCBD current implementations
|
|
// should return an error when setting CBD to true
|
|
if err := cn.ModifyCreateBeforeDestroy(true); err != nil {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Invalid planned change",
|
|
fmt.Sprintf("%s: wasn't able to set CBD, error occured %s", dag.VertexName(node), err.Error())))
|
|
log.Printf("[TRACE] DiffTransformer: %s: wasn't able to set CBD, error occured %s", addr, err.Error())
|
|
continue
|
|
}
|
|
}
|
|
|
|
log.Printf("[TRACE] DiffTransformer: %s will be represented by %s, deposing prior object to %s", addr, dag.VertexName(node), dk)
|
|
} else {
|
|
log.Printf("[TRACE] DiffTransformer: %s will be represented by %s", addr, dag.VertexName(node))
|
|
}
|
|
|
|
g.Add(node)
|
|
rsrcAddr := addr.ContainingResource().String()
|
|
for _, rsrcNode := range resourceNodes[rsrcAddr] {
|
|
g.Connect(dag.BasicEdge(node, rsrcNode))
|
|
}
|
|
|
|
if forget {
|
|
// We need to first forget the resource instance and make anew
|
|
forgetNode := produceForgetNode(addr, dk)
|
|
g.Add(forgetNode)
|
|
g.Connect(dag.BasicEdge(node, forgetNode))
|
|
}
|
|
}
|
|
|
|
if delete {
|
|
// Destroying always uses a destroy-specific node type, though
|
|
// which one depends on whether we're destroying a current object
|
|
// or a deposed object.
|
|
var node GraphNodeResourceInstance
|
|
abstract := NewNodeAbstractResourceInstance(addr)
|
|
if dk == states.NotDeposed {
|
|
// If any removed block is targeting the resource in this node, ensure that any provisioners defined in that block are going to be
|
|
// executed before actual resource destruction.
|
|
abstract.removedBlockProvisioners = refactoring.FindResourceRemovedBlockProvisioners(t.Config, abstract.Addr.ConfigResource())
|
|
node = &NodeDestroyResourceInstance{
|
|
NodeAbstractResourceInstance: abstract,
|
|
DeposedKey: dk,
|
|
}
|
|
} else {
|
|
node = &NodeDestroyDeposedResourceInstanceObject{
|
|
NodeAbstractResourceInstance: abstract,
|
|
DeposedKey: dk,
|
|
}
|
|
}
|
|
if dk == states.NotDeposed {
|
|
log.Printf("[TRACE] DiffTransformer: %s will be represented for destruction by %s", addr, dag.VertexName(node))
|
|
} else {
|
|
log.Printf("[TRACE] DiffTransformer: %s deposed object %s will be represented for destruction by %s", addr, dk, dag.VertexName(node))
|
|
}
|
|
g.Add(node)
|
|
}
|
|
|
|
if forget && !update {
|
|
g.Add(produceForgetNode(addr, dk))
|
|
}
|
|
|
|
}
|
|
|
|
log.Printf("[TRACE] DiffTransformer complete")
|
|
|
|
return diags.Err()
|
|
}
|
|
|
|
func produceForgetNode(addr addrs.AbsResourceInstance, deposedKey states.DeposedKey) GraphNodeResourceInstance {
|
|
var node GraphNodeResourceInstance
|
|
abstract := NewNodeAbstractResourceInstance(addr)
|
|
if deposedKey == states.NotDeposed {
|
|
node = &NodeForgetResourceInstance{
|
|
NodeAbstractResourceInstance: abstract,
|
|
DeposedKey: deposedKey,
|
|
}
|
|
log.Printf("[TRACE] DiffTransformer: %s will be represented for removal from the state by %s", addr, dag.VertexName(node))
|
|
} else {
|
|
node = &NodeForgetDeposedResourceInstanceObject{
|
|
NodeAbstractResourceInstance: abstract,
|
|
DeposedKey: deposedKey,
|
|
}
|
|
log.Printf("[TRACE] DiffTransformer: %s deposed object %s will be represented for removal from the state by %s", addr, deposedKey, dag.VertexName(node))
|
|
}
|
|
return node
|
|
}
|