opentofu/internal/terraform/graph.go
Martin Atkins 4bc1696fd1 core: Simplify our idea of "root node" and require it for DynamicExpand
The graph walking mechanism is specified as requiring a graph with a single
root, which in practice means there's exactly one node in the graph
which doesn't have any dependencies.

However, we previously weren't verifying that invariant is true for
subgraphs returned from DynamicExpand. It was working anyway, but it's not
ideal to be relying on a behavior that isn't guaranteed by our underlying
infrastructure.

We also previously had the RootTransformer being a bit clever and trying
to avoid adding a new node if there is already only a single graph with
no dependencies. That special case isn't particularly valuable since
there's no harm in turning a one-node graph into a two-node graph with
an explicit separate root node, and doing that allows us to assume that
the root node is always present and is always exactly terraform.rootNode.

Many existing DynamicExpand implementations were not producing valid
graphs and were previously getting away with it. All of them now produce
properly-rooted graphs that should pass validation, and we will guarantee
that with an explicit check of the DynamicExpand return value before we
try to walk that subgraph. For good measure we also verify that the root
node is exactly terraform.rootNode, even though that isn't strictly
required by our graph walker, just to help us catch potential future bugs
where a DynamicExpand implementation neglects to add our singleton root
node.
2022-10-13 14:01:08 -07:00

136 lines
4.4 KiB
Go

package terraform
import (
"fmt"
"log"
"strings"
"github.com/hashicorp/terraform/internal/logging"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/dag"
)
// Graph represents the graph that Terraform uses to represent resources
// and their dependencies.
type Graph struct {
// Graph is the actual DAG. This is embedded so you can call the DAG
// methods directly.
dag.AcyclicGraph
// Path is the path in the module tree that this Graph represents.
Path addrs.ModuleInstance
}
func (g *Graph) DirectedGraph() dag.Grapher {
return &g.AcyclicGraph
}
// Walk walks the graph with the given walker for callbacks. The graph
// will be walked with full parallelism, so the walker should expect
// to be called in concurrently.
func (g *Graph) Walk(walker GraphWalker) tfdiags.Diagnostics {
return g.walk(walker)
}
func (g *Graph) walk(walker GraphWalker) tfdiags.Diagnostics {
// The callbacks for enter/exiting a graph
ctx := walker.EvalContext()
// Walk the graph.
walkFn := func(v dag.Vertex) (diags tfdiags.Diagnostics) {
// the walkFn is called asynchronously, and needs to be recovered
// separately in the case of a panic.
defer logging.PanicHandler()
log.Printf("[TRACE] vertex %q: starting visit (%T)", dag.VertexName(v), v)
defer func() {
if diags.HasErrors() {
for _, diag := range diags {
if diag.Severity() == tfdiags.Error {
desc := diag.Description()
log.Printf("[ERROR] vertex %q error: %s", dag.VertexName(v), desc.Summary)
}
}
log.Printf("[TRACE] vertex %q: visit complete, with errors", dag.VertexName(v))
} else {
log.Printf("[TRACE] vertex %q: visit complete", dag.VertexName(v))
}
}()
// vertexCtx is the context that we use when evaluating. This
// is normally the context of our graph but can be overridden
// with a GraphNodeModuleInstance impl.
vertexCtx := ctx
if pn, ok := v.(GraphNodeModuleInstance); ok {
vertexCtx = walker.EnterPath(pn.Path())
defer walker.ExitPath(pn.Path())
}
// If the node is exec-able, then execute it.
if ev, ok := v.(GraphNodeExecutable); ok {
diags = diags.Append(walker.Execute(vertexCtx, ev))
if diags.HasErrors() {
return
}
}
// If the node is dynamically expanded, then expand it
if ev, ok := v.(GraphNodeDynamicExpandable); ok {
log.Printf("[TRACE] vertex %q: expanding dynamic subgraph", dag.VertexName(v))
g, err := ev.DynamicExpand(vertexCtx)
diags = diags.Append(err)
if diags.HasErrors() {
log.Printf("[TRACE] vertex %q: failed expanding dynamic subgraph: %s", dag.VertexName(v), err)
return
}
if g != nil {
// The subgraph should always be valid, per our normal acyclic
// graph validation rules.
if err := g.Validate(); err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Graph node has invalid dynamic subgraph",
fmt.Sprintf("The internal logic for %q generated an invalid dynamic subgraph: %s.\n\nThis is a bug in Terraform. Please report it!", dag.VertexName(v), err),
))
return
}
// If we passed validation then there is exactly one root node.
// That root node should always be "rootNode", the singleton
// root node value.
if n, err := g.Root(); err != nil || n != dag.Vertex(rootNode) {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Graph node has invalid dynamic subgraph",
fmt.Sprintf("The internal logic for %q generated an invalid dynamic subgraph: the root node is %T, which is not a suitable root node type.\n\nThis is a bug in Terraform. Please report it!", dag.VertexName(v), v),
))
return
}
// Walk the subgraph
log.Printf("[TRACE] vertex %q: entering dynamic subgraph", dag.VertexName(v))
subDiags := g.walk(walker)
diags = diags.Append(subDiags)
if subDiags.HasErrors() {
var errs []string
for _, d := range subDiags {
errs = append(errs, d.Description().Summary)
}
log.Printf("[TRACE] vertex %q: dynamic subgraph encountered errors: %s", dag.VertexName(v), strings.Join(errs, ","))
return
}
log.Printf("[TRACE] vertex %q: dynamic subgraph completed successfully", dag.VertexName(v))
} else {
log.Printf("[TRACE] vertex %q: produced no dynamic subgraph", dag.VertexName(v))
}
}
return
}
return g.AcyclicGraph.Walk(walkFn)
}