mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 01:41:48 -06:00
ce49dd6080
When you specify `-verbose` you'll get the whole graph of operations, which gives a better idea of the operations terraform performs and in what order. The DOT graph is now generated with a small internal library instead of simple string building. This allows us to ensure the graph generation is as consistent as possible, among other benefits. We set `newrank = true` in the graph, which I've found does just as good a job organizing things visually as manually attempting to rank the nodes based on depth. This also fixes `-module-depth`, which was broken post-AST refector. Modules are now expanded into subgraphs with labels and borders. We have yet to regain the plan graphing functionality, so I removed that from the docs for now. Finally, if `-draw-cycles` is added, extra colored edges will be drawn to indicate the path of any cycles detected in the graph. A notable implementation change included here is that {Reverse,}DepthFirstWalk has been made deterministic. (Before it was dependent on `map` ordering.) This turned out to be unnecessary to gain determinism in the final DOT-level implementation, but it seemed a desirable enough of a property that I left it in.
186 lines
4.2 KiB
Go
186 lines
4.2 KiB
Go
package terraform
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/terraform/dag"
|
|
"github.com/hashicorp/terraform/dot"
|
|
)
|
|
|
|
// GraphNodeDotter can be implemented by a node to cause it to be included
|
|
// in the dot graph. The Dot method will be called which is expected to
|
|
// return a representation of this node.
|
|
type GraphNodeDotter interface {
|
|
// Dot is called to return the dot formatting for the node.
|
|
// The first parameter is the title of the node.
|
|
// The second parameter includes user-specified options that affect the dot
|
|
// graph. See GraphDotOpts below for details.
|
|
DotNode(string, *GraphDotOpts) *dot.Node
|
|
}
|
|
|
|
type GraphNodeDotOrigin interface {
|
|
DotOrigin() bool
|
|
}
|
|
|
|
// GraphDotOpts are the options for generating a dot formatted Graph.
|
|
type GraphDotOpts struct {
|
|
// Allows some nodes to decide to only show themselves when the user has
|
|
// requested the "verbose" graph.
|
|
Verbose bool
|
|
|
|
// Highlight Cycles
|
|
DrawCycles bool
|
|
|
|
// How many levels to expand modules as we draw
|
|
MaxDepth int
|
|
}
|
|
|
|
// GraphDot returns the dot formatting of a visual representation of
|
|
// the given Terraform graph.
|
|
func GraphDot(g *Graph, opts *GraphDotOpts) (string, error) {
|
|
dg := dot.NewGraph(map[string]string{
|
|
"compound": "true",
|
|
"newrank": "true",
|
|
})
|
|
dg.Directed = true
|
|
|
|
err := graphDotSubgraph(dg, "root", g, opts, 0)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return dg.String(), nil
|
|
}
|
|
|
|
func graphDotSubgraph(
|
|
dg *dot.Graph, modName string, g *Graph, opts *GraphDotOpts, modDepth int) error {
|
|
// Respect user-specified module depth
|
|
if opts.MaxDepth >= 0 && modDepth > opts.MaxDepth {
|
|
return nil
|
|
}
|
|
|
|
// Begin module subgraph
|
|
var sg *dot.Subgraph
|
|
if modDepth == 0 {
|
|
sg = dg.AddSubgraph(modName)
|
|
} else {
|
|
sg = dg.AddSubgraph(modName)
|
|
sg.Cluster = true
|
|
sg.AddAttr("label", modName)
|
|
}
|
|
|
|
origins, err := graphDotFindOrigins(g)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
drawableVertices := make(map[dag.Vertex]struct{})
|
|
toDraw := make([]dag.Vertex, 0, len(g.Vertices()))
|
|
subgraphVertices := make(map[dag.Vertex]*Graph)
|
|
|
|
walk := func(v dag.Vertex, depth int) error {
|
|
// We only care about nodes that yield non-empty Dot strings.
|
|
if dn, ok := v.(GraphNodeDotter); !ok {
|
|
return nil
|
|
} else if dn.DotNode("fake", opts) == nil {
|
|
return nil
|
|
}
|
|
|
|
drawableVertices[v] = struct{}{}
|
|
toDraw = append(toDraw, v)
|
|
|
|
if sn, ok := v.(GraphNodeSubgraph); ok {
|
|
subgraphVertices[v] = sn.Subgraph()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if err := g.ReverseDepthFirstWalk(origins, walk); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, v := range toDraw {
|
|
dn := v.(GraphNodeDotter)
|
|
nodeName := graphDotNodeName(modName, v)
|
|
sg.AddNode(dn.DotNode(nodeName, opts))
|
|
|
|
// Draw all the edges from this vertex to other nodes
|
|
targets := dag.AsVertexList(g.DownEdges(v))
|
|
for _, t := range targets {
|
|
target := t.(dag.Vertex)
|
|
// Only want edges where both sides are drawable.
|
|
if _, ok := drawableVertices[target]; !ok {
|
|
continue
|
|
}
|
|
|
|
if err := sg.AddEdgeBetween(
|
|
graphDotNodeName(modName, v),
|
|
graphDotNodeName(modName, target),
|
|
map[string]string{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recurse into any subgraphs
|
|
for _, v := range toDraw {
|
|
subgraph, ok := subgraphVertices[v]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
err := graphDotSubgraph(dg, dag.VertexName(v), subgraph, opts, modDepth+1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if opts.DrawCycles {
|
|
colors := []string{"red", "green", "blue"}
|
|
for ci, cycle := range g.Cycles() {
|
|
for i, c := range cycle {
|
|
// Catch the last wrapping edge of the cycle
|
|
if i+1 >= len(cycle) {
|
|
i = -1
|
|
}
|
|
edgeAttrs := map[string]string{
|
|
"color": colors[ci%len(colors)],
|
|
"penwidth": "2.0",
|
|
}
|
|
|
|
if err := sg.AddEdgeBetween(
|
|
graphDotNodeName(modName, c),
|
|
graphDotNodeName(modName, cycle[i+1]),
|
|
edgeAttrs); err != nil {
|
|
return err
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func graphDotNodeName(modName, v dag.Vertex) string {
|
|
return fmt.Sprintf("[%s] %s", modName, dag.VertexName(v))
|
|
}
|
|
|
|
func graphDotFindOrigins(g *Graph) ([]dag.Vertex, error) {
|
|
var origin []dag.Vertex
|
|
|
|
for _, v := range g.Vertices() {
|
|
if dr, ok := v.(GraphNodeDotOrigin); ok {
|
|
if dr.DotOrigin() {
|
|
origin = append(origin, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(origin) == 0 {
|
|
return nil, fmt.Errorf("No DOT origin nodes found.\nGraph: %s", g)
|
|
}
|
|
|
|
return origin, nil
|
|
}
|