mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 01:41:48 -06:00
386 lines
13 KiB
Go
386 lines
13 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package tofu
|
|
|
|
import (
|
|
"log"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/dag"
|
|
"github.com/opentofu/opentofu/internal/plans"
|
|
)
|
|
|
|
// GraphNodeDestroyer must be implemented by nodes that destroy resources.
|
|
type GraphNodeDestroyer interface {
|
|
dag.Vertex
|
|
|
|
// DestroyAddr is the address of the resource that is being
|
|
// destroyed by this node. If this returns nil, then this node
|
|
// is not destroying anything.
|
|
DestroyAddr() *addrs.AbsResourceInstance
|
|
}
|
|
|
|
// GraphNodeCreator must be implemented by nodes that create OR update resources.
|
|
type GraphNodeCreator interface {
|
|
// CreateAddr is the address of the resource being created or updated
|
|
CreateAddr() *addrs.AbsResourceInstance
|
|
}
|
|
|
|
// DestroyEdgeTransformer is a GraphTransformer that creates the proper
|
|
// references for destroy resources. Destroy resources are more complex
|
|
// in that they must be depend on the destruction of resources that
|
|
// in turn depend on the CREATION of the node being destroy.
|
|
//
|
|
// That is complicated. Visually:
|
|
//
|
|
// B_d -> A_d -> A -> B
|
|
//
|
|
// Notice that A destroy depends on B destroy, while B create depends on
|
|
// A create. They're inverted. This must be done for example because often
|
|
// dependent resources will block parent resources from deleting. Concrete
|
|
// example: VPC with subnets, the VPC can't be deleted while there are
|
|
// still subnets.
|
|
type DestroyEdgeTransformer struct {
|
|
// FIXME: GraphNodeCreators are not always applying changes, and should not
|
|
// participate in the destroy graph if there are no operations which could
|
|
// interract with destroy nodes. We need Changes for now to detect the
|
|
// action type, but perhaps this should be indicated somehow by the
|
|
// DiffTransformer which was intended to be the only transformer operating
|
|
// from the change set.
|
|
Changes *plans.Changes
|
|
|
|
// FIXME: Operation will not be needed here one we can better track
|
|
// inter-provider dependencies and remove the cycle checks in
|
|
// tryInterProviderDestroyEdge.
|
|
Operation walkOperation
|
|
}
|
|
|
|
// tryInterProviderDestroyEdge checks if we're inserting a destroy edge
|
|
// across a provider boundary, and only adds the edge if it results in no cycles.
|
|
//
|
|
// FIXME: The cycles can arise in valid configurations when a provider depends
|
|
// on resources from another provider. In the future we may want to inspect
|
|
// the dependencies of the providers themselves, to avoid needing to use the
|
|
// blunt hammer of checking for cycles.
|
|
//
|
|
// A reduced example of this dependency problem looks something like:
|
|
/*
|
|
|
|
createA <- createB
|
|
| \ / |
|
|
| providerB <- |
|
|
v \ v
|
|
destroyA -------------> destroyB
|
|
|
|
*/
|
|
//
|
|
// The edge from destroyA to destroyB would be skipped in this case, but there
|
|
// are still other combinations of changes which could connect the A and B
|
|
// groups around providerB in various ways.
|
|
//
|
|
// The most difficult problem here happens during a full destroy operation.
|
|
// That creates a special case where resources on which a provider depends must
|
|
// exist for evaluation before they are destroyed. This means that any provider
|
|
// dependencies must wait until all that provider's resources have first been
|
|
// destroyed. This is where these cross-provider edges are still required to
|
|
// ensure the correct order.
|
|
func (t *DestroyEdgeTransformer) tryInterProviderDestroyEdge(g *Graph, from, to dag.Vertex) {
|
|
e := dag.BasicEdge(from, to)
|
|
g.Connect(e)
|
|
|
|
// If this is a complete destroy operation, then there are no create/update
|
|
// nodes to worry about and we can accept the edge without deeper inspection.
|
|
if t.Operation == walkDestroy || t.Operation == walkPlanDestroy {
|
|
return
|
|
}
|
|
|
|
// getComparableProvider inspects the node to try and get the most precise
|
|
// description of the provider being used to help determine if 2 nodes are
|
|
// from the same provider instance.
|
|
getComparableProvider := func(pc GraphNodeProviderConsumer) string {
|
|
ps := pc.Provider().String()
|
|
|
|
// we don't care about `exact` here, since we're only looking for any
|
|
// clue that the providers may differ.
|
|
p, _ := pc.ProvidedBy()
|
|
switch p := p.(type) {
|
|
case addrs.AbsProviderConfig:
|
|
ps = p.String()
|
|
case addrs.LocalProviderConfig:
|
|
ps = p.String()
|
|
}
|
|
|
|
return ps
|
|
}
|
|
|
|
pc, ok := from.(GraphNodeProviderConsumer)
|
|
if !ok {
|
|
return
|
|
}
|
|
fromProvider := getComparableProvider(pc)
|
|
|
|
pc, ok = to.(GraphNodeProviderConsumer)
|
|
if !ok {
|
|
return
|
|
}
|
|
toProvider := getComparableProvider(pc)
|
|
|
|
// Check for cycles, and back out the edge if there are any.
|
|
// The cycles we are looking for only appears between providers, so don't
|
|
// waste time checking for cycles if both nodes use the same provider.
|
|
if fromProvider != toProvider && len(g.Cycles()) > 0 {
|
|
log.Printf("[DEBUG] DestroyEdgeTransformer: skipping inter-provider edge %s->%s which creates a cycle",
|
|
dag.VertexName(from), dag.VertexName(to))
|
|
g.RemoveEdge(e)
|
|
}
|
|
}
|
|
|
|
func (t *DestroyEdgeTransformer) Transform(g *Graph) error {
|
|
// Build a map of what is being destroyed (by address string) to
|
|
// the list of destroyers.
|
|
destroyers := make(map[string][]GraphNodeDestroyer)
|
|
|
|
// Record the creators, which will need to depend on the destroyers if they
|
|
// are only being updated.
|
|
creators := make(map[string][]GraphNodeCreator)
|
|
|
|
// destroyersByResource records each destroyer by the ConfigResource
|
|
// address. We use this because dependencies are only referenced as
|
|
// resources and have no index or module instance information, but we will
|
|
// want to connect all the individual instances for correct ordering.
|
|
destroyersByResource := make(map[string][]GraphNodeDestroyer)
|
|
for _, v := range g.Vertices() {
|
|
switch n := v.(type) {
|
|
case GraphNodeDestroyer:
|
|
addrP := n.DestroyAddr()
|
|
if addrP == nil {
|
|
log.Printf("[WARN] DestroyEdgeTransformer: %q (%T) has no destroy address", dag.VertexName(n), v)
|
|
continue
|
|
}
|
|
addr := *addrP
|
|
|
|
key := addr.String()
|
|
log.Printf("[TRACE] DestroyEdgeTransformer: %q (%T) destroys %s", dag.VertexName(n), v, key)
|
|
destroyers[key] = append(destroyers[key], n)
|
|
|
|
resAddr := addr.ContainingResource().Config().String()
|
|
destroyersByResource[resAddr] = append(destroyersByResource[resAddr], n)
|
|
case GraphNodeCreator:
|
|
addr := n.CreateAddr()
|
|
cfgAddr := addr.ContainingResource().Config().String()
|
|
|
|
if t.Changes == nil {
|
|
// unit tests may not have changes
|
|
creators[cfgAddr] = append(creators[cfgAddr], n)
|
|
break
|
|
}
|
|
|
|
// NoOp changes should not participate in the destroy dependencies.
|
|
rc := t.Changes.ResourceInstance(*addr)
|
|
if rc != nil && rc.Action != plans.NoOp {
|
|
creators[cfgAddr] = append(creators[cfgAddr], n)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we aren't destroying anything, there will be no edges to make
|
|
// so just exit early and avoid future work.
|
|
if len(destroyers) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Go through and connect creators to destroyers. Going along with
|
|
// our example, this makes: A_d => A
|
|
for _, v := range g.Vertices() {
|
|
cn, ok := v.(GraphNodeCreator)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
addr := cn.CreateAddr()
|
|
if addr == nil {
|
|
continue
|
|
}
|
|
|
|
for _, d := range destroyers[addr.String()] {
|
|
// For illustrating our example
|
|
a_d := d.(dag.Vertex)
|
|
a := v
|
|
|
|
log.Printf(
|
|
"[TRACE] DestroyEdgeTransformer: connecting creator %q with destroyer %q",
|
|
dag.VertexName(a), dag.VertexName(a_d))
|
|
|
|
g.Connect(dag.BasicEdge(a, a_d))
|
|
}
|
|
}
|
|
|
|
// connect creators to any destroyers on which they may depend
|
|
for _, cs := range creators {
|
|
for _, c := range cs {
|
|
ri, ok := c.(GraphNodeResourceInstance)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for _, resAddr := range ri.StateDependencies() {
|
|
for _, desDep := range destroyersByResource[resAddr.String()] {
|
|
if !graphNodesAreResourceInstancesInDifferentInstancesOfSameModule(c, desDep) {
|
|
log.Printf("[TRACE] DestroyEdgeTransformer: %s has stored dependency of %s\n", dag.VertexName(c), dag.VertexName(desDep))
|
|
g.Connect(dag.BasicEdge(c, desDep))
|
|
} else {
|
|
log.Printf("[TRACE] DestroyEdgeTransformer: skipping %s => %s inter-module-instance dependency\n", dag.VertexName(c), dag.VertexName(desDep))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Connect destroy dependencies as stored in the state
|
|
for _, ds := range destroyers {
|
|
for _, des := range ds {
|
|
ri, ok := des.(GraphNodeResourceInstance)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for _, resAddr := range ri.StateDependencies() {
|
|
for _, desDep := range destroyersByResource[resAddr.String()] {
|
|
if !graphNodesAreResourceInstancesInDifferentInstancesOfSameModule(desDep, des) {
|
|
log.Printf("[TRACE] DestroyEdgeTransformer: %s has stored dependency of %s\n", dag.VertexName(desDep), dag.VertexName(des))
|
|
t.tryInterProviderDestroyEdge(g, desDep, des)
|
|
} else {
|
|
log.Printf("[TRACE] DestroyEdgeTransformer: skipping %s => %s inter-module-instance dependency\n", dag.VertexName(desDep), dag.VertexName(des))
|
|
}
|
|
}
|
|
|
|
// We can have some create or update nodes which were
|
|
// dependents of the destroy node. If they have no destroyer
|
|
// themselves, make the connection directly from the creator.
|
|
for _, createDep := range creators[resAddr.String()] {
|
|
if !graphNodesAreResourceInstancesInDifferentInstancesOfSameModule(createDep, des) {
|
|
log.Printf("[DEBUG] DestroyEdgeTransformer2: %s has stored dependency of %s\n", dag.VertexName(createDep), dag.VertexName(des))
|
|
t.tryInterProviderDestroyEdge(g, createDep, des)
|
|
} else {
|
|
log.Printf("[TRACE] DestroyEdgeTransformer2: skipping %s => %s inter-module-instance dependency\n", dag.VertexName(createDep), dag.VertexName(des))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Remove any nodes that aren't needed when destroying modules.
|
|
// Variables, outputs, locals, and expanders may not be able to evaluate
|
|
// correctly, so we can remove these if nothing depends on them. The module
|
|
// closers also need to disable their use of expansion if the module itself is
|
|
// no longer present.
|
|
type pruneUnusedNodesTransformer struct {
|
|
// The plan graph builder will skip this transformer except during a full
|
|
// destroy. Planing normally involves all nodes, but during a destroy plan
|
|
// we may need to prune things which are in the configuration but do not
|
|
// exist in state to evaluate.
|
|
skip bool
|
|
}
|
|
|
|
func (t *pruneUnusedNodesTransformer) Transform(g *Graph) error {
|
|
if t.skip {
|
|
return nil
|
|
}
|
|
|
|
// We need a reverse depth first walk of modules, processing them in order
|
|
// from the leaf modules to the root. This allows us to remove unneeded
|
|
// dependencies from child modules, freeing up nodes in the parent module
|
|
// to also be removed.
|
|
|
|
nodes := g.Vertices()
|
|
|
|
for removed := true; removed; {
|
|
removed = false
|
|
|
|
for i := 0; i < len(nodes); i++ {
|
|
// run this in a closure, so we can return early rather than
|
|
// dealing with complex looping and labels
|
|
func() {
|
|
n := nodes[i]
|
|
switch n := n.(type) {
|
|
case graphNodeTemporaryValue:
|
|
// root module outputs indicate they are not temporary by
|
|
// returning false here.
|
|
if !n.temporaryValue() {
|
|
return
|
|
}
|
|
|
|
// temporary values, which consist of variables, locals,
|
|
// and outputs, must be kept if anything refers to them.
|
|
for _, v := range g.UpEdges(n) {
|
|
// keep any value which is connected through a
|
|
// reference
|
|
if _, ok := v.(GraphNodeReferencer); ok {
|
|
return
|
|
}
|
|
}
|
|
|
|
case graphNodeExpandsInstances:
|
|
// Any nodes that expand instances are kept when their
|
|
// instances may need to be evaluated.
|
|
for _, v := range g.UpEdges(n) {
|
|
switch v.(type) {
|
|
case graphNodeExpandsInstances:
|
|
// Root module output values (which the following
|
|
// condition matches) are exempt because we know
|
|
// there is only ever exactly one instance of the
|
|
// root module, and so it's not actually important
|
|
// to expand it and so this lets us do a bit more
|
|
// pruning than we'd be able to do otherwise.
|
|
if tmp, ok := v.(graphNodeTemporaryValue); ok && !tmp.temporaryValue() {
|
|
continue
|
|
}
|
|
|
|
// expanders can always depend on module expansion
|
|
// themselves
|
|
return
|
|
case GraphNodeResourceInstance:
|
|
// resource instances always depend on their
|
|
// resource node, which is an expander
|
|
return
|
|
}
|
|
}
|
|
|
|
case GraphNodeProvider:
|
|
// Only keep providers for evaluation if they have
|
|
// resources to handle.
|
|
// The provider transformers removed most unused providers
|
|
// earlier, however there may be more to prune now based on
|
|
// targeting or a destroy with no related instances in the
|
|
// state.
|
|
des, _ := g.Descendents(n)
|
|
for _, v := range des {
|
|
switch v.(type) {
|
|
case GraphNodeProviderConsumer:
|
|
return
|
|
}
|
|
}
|
|
|
|
default:
|
|
return
|
|
}
|
|
|
|
log.Printf("[DEBUG] pruneUnusedNodes: %s is no longer needed, removing", dag.VertexName(n))
|
|
g.Remove(n)
|
|
removed = true
|
|
|
|
// remove the node from our iteration as well
|
|
last := len(nodes) - 1
|
|
nodes[i], nodes[last] = nodes[last], nodes[i]
|
|
nodes = nodes[:last]
|
|
}()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|