opentofu/terraform/transform_destroy_cbd.go
Martin Atkins c937c06a03 terraform: ugly huge change to weave in new HCL2-oriented types
Due to how deeply the configuration types go into Terraform Core, there
isn't a great way to switch out to HCL2 gradually. As a consequence, this
huge commit gets us from the old state to a _compilable_ new state, but
does not yet attempt to fix any tests and has a number of known missing
parts and bugs. We will continue to iterate on this in forthcoming
commits, heading back towards passing tests and making Terraform
fully-functional again.

The three main goals here are:
- Use the configuration models from the "configs" package instead of the
  older models in the "config" package, which is now deprecated and
  preserved only to help us write our migration tool.
- Do expression inspection and evaluation using the functionality of the
  new "lang" package, instead of the Interpolator type and related
  functionality in the main "terraform" package.
- Represent addresses of various objects using types in the addrs package,
  rather than hand-constructed strings. This is not critical to support
  the above, but was a big help during the implementation of these other
  points since it made it much more explicit what kind of address is
  expected in each context.

Since our new packages are built to accommodate some future planned
features that are not yet implemented (e.g. the "for_each" argument on
resources, "count"/"for_each" on modules), and since there's still a fair
amount of functionality still using old-style APIs, there is a moderate
amount of shimming here to connect new assumptions with old, hopefully in
a way that makes it easier to find and eliminate these shims later.

I apologize in advance to the person who inevitably just found this huge
commit while spelunking through the commit history.
2018-10-16 18:46:46 -07:00

249 lines
7.4 KiB
Go

package terraform
import (
"fmt"
"log"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/dag"
)
// GraphNodeDestroyerCBD must be implemented by nodes that might be
// create-before-destroy destroyers.
type GraphNodeDestroyerCBD interface {
GraphNodeDestroyer
// CreateBeforeDestroy returns true if this node represents a node
// that is doing a CBD.
CreateBeforeDestroy() bool
// ModifyCreateBeforeDestroy is called when the CBD state of a node
// is changed dynamically. This can return an error if this isn't
// allowed.
ModifyCreateBeforeDestroy(bool) error
}
// CBDEdgeTransformer modifies the edges of CBD nodes that went through
// the DestroyEdgeTransformer to have the right dependencies. There are
// two real tasks here:
//
// 1. With CBD, the destroy edge is inverted: the destroy depends on
// the creation.
//
// 2. A_d must depend on resources that depend on A. This is to enable
// the destroy to only happen once nodes that depend on A successfully
// update to A. Example: adding a web server updates the load balancer
// before deleting the old web server.
//
type CBDEdgeTransformer struct {
// Module and State are only needed to look up dependencies in
// any way possible. Either can be nil if not availabile.
Config *configs.Config
State *State
}
func (t *CBDEdgeTransformer) Transform(g *Graph) error {
log.Printf("[TRACE] CBDEdgeTransformer: Beginning CBD transformation...")
// Go through and reverse any destroy edges
destroyMap := make(map[string][]dag.Vertex)
for _, v := range g.Vertices() {
dn, ok := v.(GraphNodeDestroyerCBD)
if !ok {
continue
}
if !dn.CreateBeforeDestroy() {
// If there are no CBD ancestors (dependent nodes), then we
// do nothing here.
if !t.hasCBDAncestor(g, v) {
continue
}
// If this isn't naturally a CBD node, this means that an ancestor is
// and we need to auto-upgrade this node to CBD. We do this because
// a CBD node depending on non-CBD will result in cycles. To avoid this,
// we always attempt to upgrade it.
if err := dn.ModifyCreateBeforeDestroy(true); err != nil {
return fmt.Errorf(
"%s: must have create before destroy enabled because "+
"a dependent resource has CBD enabled. However, when "+
"attempting to automatically do this, an error occurred: %s",
dag.VertexName(v), err)
}
}
// Find the destroy edge. There should only be one.
for _, e := range g.EdgesTo(v) {
// Not a destroy edge, ignore it
de, ok := e.(*DestroyEdge)
if !ok {
continue
}
log.Printf("[TRACE] CBDEdgeTransformer: inverting edge: %s => %s",
dag.VertexName(de.Source()), dag.VertexName(de.Target()))
// Found it! Invert.
g.RemoveEdge(de)
g.Connect(&DestroyEdge{S: de.Target(), T: de.Source()})
}
// If the address has an index, we strip that. Our depMap creation
// graph doesn't expand counts so we don't currently get _exact_
// dependencies. One day when we limit dependencies more exactly
// this will have to change. We have a test case covering this
// (depNonCBDCountBoth) so it'll be caught.
addr := dn.DestroyAddr()
key := addr.ContainingResource().String()
// Add this to the list of nodes that we need to fix up
// the edges for (step 2 above in the docs).
destroyMap[key] = append(destroyMap[key], v)
}
// If we have no CBD nodes, then our work here is done
if len(destroyMap) == 0 {
return nil
}
// We have CBD nodes. We now have to move on to the much more difficult
// task of connecting dependencies of the creation side of the destroy
// to the destruction node. The easiest way to explain this is an example:
//
// Given a pre-destroy dependence of: A => B
// And A has CBD set.
//
// The resulting graph should be: A => B => A_d
//
// They key here is that B happens before A is destroyed. This is to
// facilitate the primary purpose for CBD: making sure that downstreams
// are properly updated to avoid downtime before the resource is destroyed.
//
// We can't trust that the resource being destroyed or anything that
// depends on it is actually in our current graph so we make a new
// graph in order to determine those dependencies and add them in.
log.Printf("[TRACE] CBDEdgeTransformer: building graph to find dependencies...")
depMap, err := t.depMap(destroyMap)
if err != nil {
return err
}
// We now have the mapping of resource addresses to the destroy
// nodes they need to depend on. We now go through our own vertices to
// find any matching these addresses and make the connection.
for _, v := range g.Vertices() {
// We're looking for creators
rn, ok := v.(GraphNodeCreator)
if !ok {
continue
}
// Get the address
addr := rn.CreateAddr()
// If the address has an index, we strip that. Our depMap creation
// graph doesn't expand counts so we don't currently get _exact_
// dependencies. One day when we limit dependencies more exactly
// this will have to change. We have a test case covering this
// (depNonCBDCount) so it'll be caught.
key := addr.ContainingResource().String()
// If there is nothing this resource should depend on, ignore it
dns, ok := depMap[key]
if !ok {
continue
}
// We have nodes! Make the connection
for _, dn := range dns {
log.Printf("[TRACE] CBDEdgeTransformer: destroy depends on dependence: %s => %s",
dag.VertexName(dn), dag.VertexName(v))
g.Connect(dag.BasicEdge(dn, v))
}
}
return nil
}
func (t *CBDEdgeTransformer) depMap(destroyMap map[string][]dag.Vertex) (map[string][]dag.Vertex, error) {
// Build the graph of our config, this ensures that all resources
// are present in the graph.
g, diags := (&BasicGraphBuilder{
Steps: []GraphTransformer{
&FlatConfigTransformer{Config: t.Config},
&AttachResourceConfigTransformer{Config: t.Config},
&AttachStateTransformer{State: t.State},
&ReferenceTransformer{},
},
Name: "CBDEdgeTransformer",
}).Build(nil)
if diags.HasErrors() {
return nil, diags.Err()
}
// Using this graph, build the list of destroy nodes that each resource
// address should depend on. For example, when we find B, we map the
// address of B to A_d in the "depMap" variable below.
depMap := make(map[string][]dag.Vertex)
for _, v := range g.Vertices() {
// We're looking for resources.
rn, ok := v.(GraphNodeResource)
if !ok {
continue
}
// Get the address
addr := rn.ResourceAddr()
key := addr.String()
// Get the destroy nodes that are destroying this resource.
// If there aren't any, then we don't need to worry about
// any connections.
dns, ok := destroyMap[key]
if !ok {
continue
}
// Get the nodes that depend on this on. In the example above:
// finding B in A => B.
for _, v := range g.UpEdges(v).List() {
// We're looking for resources.
rn, ok := v.(GraphNodeResource)
if !ok {
continue
}
// Keep track of the destroy nodes that this address
// needs to depend on.
key := rn.ResourceAddr().String()
depMap[key] = append(depMap[key], dns...)
}
}
return depMap, nil
}
// hasCBDAncestor returns true if any ancestor (node that depends on this)
// has CBD set.
func (t *CBDEdgeTransformer) hasCBDAncestor(g *Graph, v dag.Vertex) bool {
s, _ := g.Ancestors(v)
if s == nil {
return true
}
for _, v := range s.List() {
dn, ok := v.(GraphNodeDestroyerCBD)
if !ok {
continue
}
if dn.CreateBeforeDestroy() {
// some ancestor is CreateBeforeDestroy, so we need to follow suit
return true
}
}
return false
}