mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 08:21:07 -06:00
a127607a85
Signed-off-by: Dmitry Kisler <admin@dkisler.com>
218 lines
7.3 KiB
Go
218 lines
7.3 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package tofu
|
|
|
|
import (
|
|
"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/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(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, 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.DeleteThenCreate, plans.CreateThenDelete:
|
|
update = true
|
|
delete = true
|
|
createBeforeDestroy = (rc.Action == plans.CreateThenDelete)
|
|
default:
|
|
update = true
|
|
}
|
|
|
|
// A deposed instance may only have a change of Delete 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) {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Invalid planned change for deposed object",
|
|
fmt.Sprintf("The plan contains a non-delete change for %s deposed object %s. The only valid action for a deposed object is to destroy 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 update {
|
|
// All actions except destroying the node type chosen by t.Concrete
|
|
abstract := NewNodeAbstractResourceInstance(addr)
|
|
var node dag.Vertex = abstract
|
|
if f := t.Concrete; f != nil {
|
|
node = f(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)
|
|
}
|
|
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 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 {
|
|
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)
|
|
}
|
|
|
|
}
|
|
|
|
log.Printf("[TRACE] DiffTransformer complete")
|
|
|
|
return diags.Err()
|
|
}
|