core: graph command gets -verbose and -draw-cycles
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.
This commit is contained in:
parent
d4b9362518
commit
ce49dd6080
|
@ -17,11 +17,15 @@ type GraphCommand struct {
|
||||||
|
|
||||||
func (c *GraphCommand) Run(args []string) int {
|
func (c *GraphCommand) Run(args []string) int {
|
||||||
var moduleDepth int
|
var moduleDepth int
|
||||||
|
var verbose bool
|
||||||
|
var drawCycles bool
|
||||||
|
|
||||||
args = c.Meta.process(args, false)
|
args = c.Meta.process(args, false)
|
||||||
|
|
||||||
cmdFlags := flag.NewFlagSet("graph", flag.ContinueOnError)
|
cmdFlags := flag.NewFlagSet("graph", flag.ContinueOnError)
|
||||||
cmdFlags.IntVar(&moduleDepth, "module-depth", 0, "module-depth")
|
cmdFlags.IntVar(&moduleDepth, "module-depth", 0, "module-depth")
|
||||||
|
cmdFlags.BoolVar(&verbose, "verbose", false, "verbose")
|
||||||
|
cmdFlags.BoolVar(&drawCycles, "draw-cycles", false, "draw-cycles")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
|
@ -52,28 +56,38 @@ func (c *GraphCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip validation during graph generation - we want to see the graph even if
|
||||||
|
// it is invalid for some reason.
|
||||||
g, err := ctx.Graph(&terraform.ContextGraphOpts{
|
g, err := ctx.Graph(&terraform.ContextGraphOpts{
|
||||||
Validate: true,
|
Verbose: verbose,
|
||||||
Verbose: false,
|
Validate: false,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error creating graph: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error creating graph: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Ui.Output(terraform.GraphDot(g, nil))
|
graphStr, err := terraform.GraphDot(g, &terraform.GraphDotOpts{
|
||||||
|
DrawCycles: drawCycles,
|
||||||
|
MaxDepth: moduleDepth,
|
||||||
|
Verbose: verbose,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error converting graph: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Ui.Output(graphStr)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GraphCommand) Help() string {
|
func (c *GraphCommand) Help() string {
|
||||||
helpText := `
|
helpText := `
|
||||||
Usage: terraform graph [options] PATH
|
Usage: terraform graph [options] [DIR]
|
||||||
|
|
||||||
Outputs the visual graph of Terraform resources. If the path given is
|
Outputs the visual dependency graph of Terraform resources according to
|
||||||
the path to a configuration, the dependency graph of the resources are
|
configuration files in DIR (or the current directory if omitted).
|
||||||
shown. If the path is a plan file, then the dependency graph of the
|
|
||||||
plan itself is shown.
|
|
||||||
|
|
||||||
The graph is outputted in DOT format. The typical program that can
|
The graph is outputted in DOT format. The typical program that can
|
||||||
read this format is GraphViz, but many web services are also available
|
read this format is GraphViz, but many web services are also available
|
||||||
|
@ -81,9 +95,14 @@ Usage: terraform graph [options] PATH
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
|
-draw-cycles Highlight any cycles in the graph with colored edges.
|
||||||
|
This helps when diagnosing cycle errors.
|
||||||
|
|
||||||
-module-depth=n The maximum depth to expand modules. By default this is
|
-module-depth=n The maximum depth to expand modules. By default this is
|
||||||
zero, which will not expand modules at all.
|
zero, which will not expand modules at all.
|
||||||
|
|
||||||
|
-verbose Generate a verbose, "worst-case" graph, with all nodes
|
||||||
|
for potential operations in place.
|
||||||
`
|
`
|
||||||
return strings.TrimSpace(helpText)
|
return strings.TrimSpace(helpText)
|
||||||
}
|
}
|
||||||
|
|
118
dag/dag.go
118
dag/dag.go
|
@ -2,6 +2,7 @@ package dag
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
@ -17,17 +18,21 @@ type AcyclicGraph struct {
|
||||||
// WalkFunc is the callback used for walking the graph.
|
// WalkFunc is the callback used for walking the graph.
|
||||||
type WalkFunc func(Vertex) error
|
type WalkFunc func(Vertex) error
|
||||||
|
|
||||||
|
// DepthWalkFunc is a walk function that also receives the current depth of the
|
||||||
|
// walk as an argument
|
||||||
|
type DepthWalkFunc func(Vertex, int) error
|
||||||
|
|
||||||
// Returns a Set that includes every Vertex yielded by walking down from the
|
// Returns a Set that includes every Vertex yielded by walking down from the
|
||||||
// provided starting Vertex v.
|
// provided starting Vertex v.
|
||||||
func (g *AcyclicGraph) Ancestors(v Vertex) (*Set, error) {
|
func (g *AcyclicGraph) Ancestors(v Vertex) (*Set, error) {
|
||||||
s := new(Set)
|
s := new(Set)
|
||||||
start := asVertexList(g.DownEdges(v))
|
start := AsVertexList(g.DownEdges(v))
|
||||||
memoFunc := func(v Vertex) error {
|
memoFunc := func(v Vertex, d int) error {
|
||||||
s.Add(v)
|
s.Add(v)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := g.depthFirstWalk(start, memoFunc); err != nil {
|
if err := g.DepthFirstWalk(start, memoFunc); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,13 +43,13 @@ func (g *AcyclicGraph) Ancestors(v Vertex) (*Set, error) {
|
||||||
// provided starting Vertex v.
|
// provided starting Vertex v.
|
||||||
func (g *AcyclicGraph) Descendents(v Vertex) (*Set, error) {
|
func (g *AcyclicGraph) Descendents(v Vertex) (*Set, error) {
|
||||||
s := new(Set)
|
s := new(Set)
|
||||||
start := asVertexList(g.UpEdges(v))
|
start := AsVertexList(g.UpEdges(v))
|
||||||
memoFunc := func(v Vertex) error {
|
memoFunc := func(v Vertex, d int) error {
|
||||||
s.Add(v)
|
s.Add(v)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := g.reverseDepthFirstWalk(start, memoFunc); err != nil {
|
if err := g.ReverseDepthFirstWalk(start, memoFunc); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,14 +97,13 @@ func (g *AcyclicGraph) TransitiveReduction() {
|
||||||
// v such that the edge (u,v) exists (v is a direct descendant of u).
|
// v such that the edge (u,v) exists (v is a direct descendant of u).
|
||||||
//
|
//
|
||||||
// For each v-prime reachable from v, remove the edge (u, v-prime).
|
// For each v-prime reachable from v, remove the edge (u, v-prime).
|
||||||
|
|
||||||
for _, u := range g.Vertices() {
|
for _, u := range g.Vertices() {
|
||||||
uTargets := g.DownEdges(u)
|
uTargets := g.DownEdges(u)
|
||||||
vs := asVertexList(g.DownEdges(u))
|
vs := AsVertexList(g.DownEdges(u))
|
||||||
|
|
||||||
g.depthFirstWalk(vs, func(v Vertex) error {
|
g.DepthFirstWalk(vs, func(v Vertex, d int) error {
|
||||||
shared := uTargets.Intersection(g.DownEdges(v))
|
shared := uTargets.Intersection(g.DownEdges(v))
|
||||||
for _, vPrime := range asVertexList(shared) {
|
for _, vPrime := range AsVertexList(shared) {
|
||||||
g.RemoveEdge(BasicEdge(u, vPrime))
|
g.RemoveEdge(BasicEdge(u, vPrime))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,12 +121,7 @@ func (g *AcyclicGraph) Validate() error {
|
||||||
|
|
||||||
// Look for cycles of more than 1 component
|
// Look for cycles of more than 1 component
|
||||||
var err error
|
var err error
|
||||||
var cycles [][]Vertex
|
cycles := g.Cycles()
|
||||||
for _, cycle := range StronglyConnected(&g.Graph) {
|
|
||||||
if len(cycle) > 1 {
|
|
||||||
cycles = append(cycles, cycle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(cycles) > 0 {
|
if len(cycles) > 0 {
|
||||||
for _, cycle := range cycles {
|
for _, cycle := range cycles {
|
||||||
cycleStr := make([]string, len(cycle))
|
cycleStr := make([]string, len(cycle))
|
||||||
|
@ -146,6 +145,16 @@ func (g *AcyclicGraph) Validate() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *AcyclicGraph) Cycles() [][]Vertex {
|
||||||
|
var cycles [][]Vertex
|
||||||
|
for _, cycle := range StronglyConnected(&g.Graph) {
|
||||||
|
if len(cycle) > 1 {
|
||||||
|
cycles = append(cycles, cycle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cycles
|
||||||
|
}
|
||||||
|
|
||||||
// Walk walks the graph, calling your callback as each node is visited.
|
// Walk walks the graph, calling your callback as each node is visited.
|
||||||
// This will walk nodes in parallel if it can. Because the walk is done
|
// This will walk nodes in parallel if it can. Because the walk is done
|
||||||
// in parallel, the error returned will be a multierror.
|
// in parallel, the error returned will be a multierror.
|
||||||
|
@ -175,7 +184,7 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error {
|
||||||
for _, v := range vertices {
|
for _, v := range vertices {
|
||||||
// Build our list of dependencies and the list of channels to
|
// Build our list of dependencies and the list of channels to
|
||||||
// wait on until we start executing for this vertex.
|
// wait on until we start executing for this vertex.
|
||||||
deps := asVertexList(g.DownEdges(v))
|
deps := AsVertexList(g.DownEdges(v))
|
||||||
depChs := make([]<-chan struct{}, len(deps))
|
depChs := make([]<-chan struct{}, len(deps))
|
||||||
for i, dep := range deps {
|
for i, dep := range deps {
|
||||||
depChs[i] = vertMap[dep]
|
depChs[i] = vertMap[dep]
|
||||||
|
@ -229,7 +238,7 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// simple convenience helper for converting a dag.Set to a []Vertex
|
// simple convenience helper for converting a dag.Set to a []Vertex
|
||||||
func asVertexList(s *Set) []Vertex {
|
func AsVertexList(s *Set) []Vertex {
|
||||||
rawList := s.List()
|
rawList := s.List()
|
||||||
vertexList := make([]Vertex, len(rawList))
|
vertexList := make([]Vertex, len(rawList))
|
||||||
for i, raw := range rawList {
|
for i, raw := range rawList {
|
||||||
|
@ -238,13 +247,23 @@ func asVertexList(s *Set) []Vertex {
|
||||||
return vertexList
|
return vertexList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type vertexAtDepth struct {
|
||||||
|
Vertex Vertex
|
||||||
|
Depth int
|
||||||
|
}
|
||||||
|
|
||||||
// depthFirstWalk does a depth-first walk of the graph starting from
|
// depthFirstWalk does a depth-first walk of the graph starting from
|
||||||
// the vertices in start. This is not exported now but it would make sense
|
// the vertices in start. This is not exported now but it would make sense
|
||||||
// to export this publicly at some point.
|
// to export this publicly at some point.
|
||||||
func (g *AcyclicGraph) depthFirstWalk(start []Vertex, cb WalkFunc) error {
|
func (g *AcyclicGraph) DepthFirstWalk(start []Vertex, f DepthWalkFunc) error {
|
||||||
seen := make(map[Vertex]struct{})
|
seen := make(map[Vertex]struct{})
|
||||||
frontier := make([]Vertex, len(start))
|
frontier := make([]*vertexAtDepth, len(start))
|
||||||
copy(frontier, start)
|
for i, v := range start {
|
||||||
|
frontier[i] = &vertexAtDepth{
|
||||||
|
Vertex: v,
|
||||||
|
Depth: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
for len(frontier) > 0 {
|
for len(frontier) > 0 {
|
||||||
// Pop the current vertex
|
// Pop the current vertex
|
||||||
n := len(frontier)
|
n := len(frontier)
|
||||||
|
@ -252,20 +271,24 @@ func (g *AcyclicGraph) depthFirstWalk(start []Vertex, cb WalkFunc) error {
|
||||||
frontier = frontier[:n-1]
|
frontier = frontier[:n-1]
|
||||||
|
|
||||||
// Check if we've seen this already and return...
|
// Check if we've seen this already and return...
|
||||||
if _, ok := seen[current]; ok {
|
if _, ok := seen[current.Vertex]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[current] = struct{}{}
|
seen[current.Vertex] = struct{}{}
|
||||||
|
|
||||||
// Visit the current node
|
// Visit the current node
|
||||||
if err := cb(current); err != nil {
|
if err := f(current.Vertex, current.Depth); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visit targets of this in reverse order.
|
// Visit targets of this in a consistent order.
|
||||||
targets := g.DownEdges(current).List()
|
targets := AsVertexList(g.DownEdges(current.Vertex))
|
||||||
for i := len(targets) - 1; i >= 0; i-- {
|
sort.Sort(byVertexName(targets))
|
||||||
frontier = append(frontier, targets[i].(Vertex))
|
for _, t := range targets {
|
||||||
|
frontier = append(frontier, &vertexAtDepth{
|
||||||
|
Vertex: t,
|
||||||
|
Depth: current.Depth + 1,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,10 +297,15 @@ func (g *AcyclicGraph) depthFirstWalk(start []Vertex, cb WalkFunc) error {
|
||||||
|
|
||||||
// reverseDepthFirstWalk does a depth-first walk _up_ the graph starting from
|
// reverseDepthFirstWalk does a depth-first walk _up_ the graph starting from
|
||||||
// the vertices in start.
|
// the vertices in start.
|
||||||
func (g *AcyclicGraph) reverseDepthFirstWalk(start []Vertex, cb WalkFunc) error {
|
func (g *AcyclicGraph) ReverseDepthFirstWalk(start []Vertex, f DepthWalkFunc) error {
|
||||||
seen := make(map[Vertex]struct{})
|
seen := make(map[Vertex]struct{})
|
||||||
frontier := make([]Vertex, len(start))
|
frontier := make([]*vertexAtDepth, len(start))
|
||||||
copy(frontier, start)
|
for i, v := range start {
|
||||||
|
frontier[i] = &vertexAtDepth{
|
||||||
|
Vertex: v,
|
||||||
|
Depth: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
for len(frontier) > 0 {
|
for len(frontier) > 0 {
|
||||||
// Pop the current vertex
|
// Pop the current vertex
|
||||||
n := len(frontier)
|
n := len(frontier)
|
||||||
|
@ -285,22 +313,36 @@ func (g *AcyclicGraph) reverseDepthFirstWalk(start []Vertex, cb WalkFunc) error
|
||||||
frontier = frontier[:n-1]
|
frontier = frontier[:n-1]
|
||||||
|
|
||||||
// Check if we've seen this already and return...
|
// Check if we've seen this already and return...
|
||||||
if _, ok := seen[current]; ok {
|
if _, ok := seen[current.Vertex]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[current] = struct{}{}
|
seen[current.Vertex] = struct{}{}
|
||||||
|
|
||||||
// Visit the current node
|
// Visit the current node
|
||||||
if err := cb(current); err != nil {
|
if err := f(current.Vertex, current.Depth); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visit targets of this in reverse order.
|
// Visit targets of this in a consistent order.
|
||||||
targets := g.UpEdges(current).List()
|
targets := AsVertexList(g.UpEdges(current.Vertex))
|
||||||
for i := len(targets) - 1; i >= 0; i-- {
|
sort.Sort(byVertexName(targets))
|
||||||
frontier = append(frontier, targets[i].(Vertex))
|
for _, t := range targets {
|
||||||
|
frontier = append(frontier, &vertexAtDepth{
|
||||||
|
Vertex: t,
|
||||||
|
Depth: current.Depth + 1,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// byVertexName implements sort.Interface so a list of Vertices can be sorted
|
||||||
|
// consistently by their VertexName
|
||||||
|
type byVertexName []Vertex
|
||||||
|
|
||||||
|
func (b byVertexName) Len() int { return len(b) }
|
||||||
|
func (b byVertexName) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||||
|
func (b byVertexName) Less(i, j int) bool {
|
||||||
|
return VertexName(b[i]) < VertexName(b[j])
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,224 @@
|
||||||
|
// The dot package contains utilities for working with DOT graphs.
|
||||||
|
package dot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Graph is a representation of a drawable DOT graph.
|
||||||
|
type Graph struct {
|
||||||
|
// Whether this is a "digraph" or just a "graph"
|
||||||
|
Directed bool
|
||||||
|
|
||||||
|
// Used for K/V settings in the DOT
|
||||||
|
Attrs map[string]string
|
||||||
|
|
||||||
|
Nodes []*Node
|
||||||
|
Edges []*Edge
|
||||||
|
Subgraphs []*Subgraph
|
||||||
|
|
||||||
|
nodesByName map[string]*Node
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subgraph is a Graph that lives inside a Parent graph, and contains some
|
||||||
|
// additional parameters to control how it is drawn.
|
||||||
|
type Subgraph struct {
|
||||||
|
Graph
|
||||||
|
Name string
|
||||||
|
Parent *Graph
|
||||||
|
Cluster bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Edge in a DOT graph, as expressed by recording the Name of the Node at
|
||||||
|
// each end.
|
||||||
|
type Edge struct {
|
||||||
|
// Name of source node.
|
||||||
|
Source string
|
||||||
|
|
||||||
|
// Name of dest node.
|
||||||
|
Dest string
|
||||||
|
|
||||||
|
// List of K/V attributes for this edge.
|
||||||
|
Attrs map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Node in a DOT graph.
|
||||||
|
type Node struct {
|
||||||
|
Name string
|
||||||
|
Attrs map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a properly initialized DOT Graph.
|
||||||
|
func NewGraph(attrs map[string]string) *Graph {
|
||||||
|
return &Graph{
|
||||||
|
Attrs: attrs,
|
||||||
|
nodesByName: make(map[string]*Node),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEdge(src, dst string, attrs map[string]string) *Edge {
|
||||||
|
return &Edge{
|
||||||
|
Source: src,
|
||||||
|
Dest: dst,
|
||||||
|
Attrs: attrs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNode(n string, attrs map[string]string) *Node {
|
||||||
|
return &Node{
|
||||||
|
Name: n,
|
||||||
|
Attrs: attrs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializes a Subgraph with the provided name, attaches is to this Graph,
|
||||||
|
// and returns it.
|
||||||
|
func (g *Graph) AddSubgraph(name string) *Subgraph {
|
||||||
|
subgraph := &Subgraph{
|
||||||
|
Graph: *NewGraph(map[string]string{}),
|
||||||
|
Parent: g,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
g.Subgraphs = append(g.Subgraphs, subgraph)
|
||||||
|
return subgraph
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Graph) AddAttr(k, v string) {
|
||||||
|
g.Attrs[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Graph) AddNode(n *Node) {
|
||||||
|
g.Nodes = append(g.Nodes, n)
|
||||||
|
g.nodesByName[n.Name] = n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Graph) AddEdge(e *Edge) {
|
||||||
|
g.Edges = append(g.Edges, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds an edge between two Nodes.
|
||||||
|
//
|
||||||
|
// Note this does not do any verification of the existence of these nodes,
|
||||||
|
// which means that any strings you provide that are not existing nodes will
|
||||||
|
// result in extra auto-defined nodes in your resulting DOT.
|
||||||
|
func (g *Graph) AddEdgeBetween(src, dst string, attrs map[string]string) error {
|
||||||
|
g.AddEdge(NewEdge(src, dst, attrs))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up a node by name
|
||||||
|
func (g *Graph) GetNode(name string) (*Node, error) {
|
||||||
|
node, ok := g.nodesByName[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("Could not find node: %s", name)
|
||||||
|
}
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the DOT representation of this Graph.
|
||||||
|
func (g *Graph) String() string {
|
||||||
|
w := newGraphWriter()
|
||||||
|
|
||||||
|
g.drawHeader(w)
|
||||||
|
w.Indent()
|
||||||
|
g.drawBody(w)
|
||||||
|
w.Unindent()
|
||||||
|
g.drawFooter(w)
|
||||||
|
|
||||||
|
return w.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Graph) drawHeader(w *graphWriter) {
|
||||||
|
if g.Directed {
|
||||||
|
w.Printf("digraph {\n")
|
||||||
|
} else {
|
||||||
|
w.Printf("graph {\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Graph) drawBody(w *graphWriter) {
|
||||||
|
for _, as := range attrStrings(g.Attrs) {
|
||||||
|
w.Printf("%s\n", as)
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeStrings := make([]string, 0, len(g.Nodes))
|
||||||
|
for _, n := range g.Nodes {
|
||||||
|
nodeStrings = append(nodeStrings, n.String())
|
||||||
|
}
|
||||||
|
sort.Strings(nodeStrings)
|
||||||
|
for _, ns := range nodeStrings {
|
||||||
|
w.Printf(ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeStrings := make([]string, 0, len(g.Edges))
|
||||||
|
for _, e := range g.Edges {
|
||||||
|
edgeStrings = append(edgeStrings, e.String())
|
||||||
|
}
|
||||||
|
sort.Strings(edgeStrings)
|
||||||
|
for _, es := range edgeStrings {
|
||||||
|
w.Printf(es)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range g.Subgraphs {
|
||||||
|
s.drawHeader(w)
|
||||||
|
w.Indent()
|
||||||
|
s.drawBody(w)
|
||||||
|
w.Unindent()
|
||||||
|
s.drawFooter(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Graph) drawFooter(w *graphWriter) {
|
||||||
|
w.Printf("}\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the DOT representation of this Edge.
|
||||||
|
func (e *Edge) String() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"%q -> %q", e.Source, e.Dest))
|
||||||
|
writeAttrs(&buf, e.Attrs)
|
||||||
|
buf.WriteString("\n")
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subgraph) drawHeader(w *graphWriter) {
|
||||||
|
name := s.Name
|
||||||
|
if s.Cluster {
|
||||||
|
name = fmt.Sprintf("cluster_%s", name)
|
||||||
|
}
|
||||||
|
w.Printf("subgraph %q {\n", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the DOT representation of this Node.
|
||||||
|
func (n *Node) String() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString(fmt.Sprintf("%q", n.Name))
|
||||||
|
writeAttrs(&buf, n.Attrs)
|
||||||
|
buf.WriteString("\n")
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAttrs(buf *bytes.Buffer, attrs map[string]string) {
|
||||||
|
if len(attrs) > 0 {
|
||||||
|
buf.WriteString(" [")
|
||||||
|
buf.WriteString(strings.Join(attrStrings(attrs), ", "))
|
||||||
|
buf.WriteString("]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func attrStrings(attrs map[string]string) []string {
|
||||||
|
strings := make([]string, 0, len(attrs))
|
||||||
|
for k, v := range attrs {
|
||||||
|
strings = append(strings, fmt.Sprintf("%s = %q", k, v))
|
||||||
|
}
|
||||||
|
sort.Strings(strings)
|
||||||
|
return strings
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package dot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// graphWriter wraps a bytes.Buffer and tracks indent level levels.
|
||||||
|
type graphWriter struct {
|
||||||
|
bytes.Buffer
|
||||||
|
indent int
|
||||||
|
indentStr string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns an initialized graphWriter at indent level 0.
|
||||||
|
func newGraphWriter() *graphWriter {
|
||||||
|
w := &graphWriter{
|
||||||
|
indent: 0,
|
||||||
|
}
|
||||||
|
w.init()
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints to the buffer at the current indent level.
|
||||||
|
func (w *graphWriter) Printf(s string, args ...interface{}) {
|
||||||
|
w.WriteString(w.indentStr + fmt.Sprintf(s, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase the indent level.
|
||||||
|
func (w *graphWriter) Indent() {
|
||||||
|
w.indent++
|
||||||
|
w.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrease the indent level.
|
||||||
|
func (w *graphWriter) Unindent() {
|
||||||
|
w.indent--
|
||||||
|
w.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *graphWriter) init() {
|
||||||
|
indentBuf := new(bytes.Buffer)
|
||||||
|
for i := 0; i < w.indent; i++ {
|
||||||
|
indentBuf.WriteString("\t")
|
||||||
|
}
|
||||||
|
w.indentStr = indentBuf.String()
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
"github.com/hashicorp/terraform/config/module"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
"github.com/hashicorp/terraform/dag"
|
"github.com/hashicorp/terraform/dag"
|
||||||
|
"github.com/hashicorp/terraform/dot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// graphNodeConfig is an interface that all graph nodes for the
|
// graphNodeConfig is an interface that all graph nodes for the
|
||||||
|
@ -219,14 +220,16 @@ func (n *GraphNodeConfigProvider) ProviderConfig() *config.RawConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GraphNodeDotter impl.
|
// GraphNodeDotter impl.
|
||||||
func (n *GraphNodeConfigProvider) Dot(name string) string {
|
func (n *GraphNodeConfigProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node {
|
||||||
return fmt.Sprintf(
|
return dot.NewNode(name, map[string]string{
|
||||||
"\"%s\" [\n"+
|
"label": n.Name(),
|
||||||
"\tlabel=\"%s\"\n"+
|
"shape": "diamond",
|
||||||
"\tshape=diamond\n"+
|
})
|
||||||
"];",
|
}
|
||||||
name,
|
|
||||||
n.Name())
|
// GraphNodeDotterOrigin impl.
|
||||||
|
func (n *GraphNodeConfigProvider) DotOrigin() bool {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// GraphNodeConfigResource represents a resource within the config graph.
|
// GraphNodeConfigResource represents a resource within the config graph.
|
||||||
|
@ -318,18 +321,14 @@ func (n *GraphNodeConfigResource) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GraphNodeDotter impl.
|
// GraphNodeDotter impl.
|
||||||
func (n *GraphNodeConfigResource) Dot(name string) string {
|
func (n *GraphNodeConfigResource) DotNode(name string, opts *GraphDotOpts) *dot.Node {
|
||||||
if n.DestroyMode != DestroyNone {
|
if n.DestroyMode != DestroyNone && !opts.Verbose {
|
||||||
return ""
|
return nil
|
||||||
}
|
}
|
||||||
|
return dot.NewNode(name, map[string]string{
|
||||||
return fmt.Sprintf(
|
"label": n.Name(),
|
||||||
"\"%s\" [\n"+
|
"shape": "box",
|
||||||
"\tlabel=\"%s\"\n"+
|
})
|
||||||
"\tshape=box\n"+
|
|
||||||
"];",
|
|
||||||
name,
|
|
||||||
n.Name())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GraphNodeDynamicExpandable impl.
|
// GraphNodeDynamicExpandable impl.
|
||||||
|
@ -635,14 +634,11 @@ func (n *graphNodeModuleExpanded) ConfigType() GraphNodeConfigType {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GraphNodeDotter impl.
|
// GraphNodeDotter impl.
|
||||||
func (n *graphNodeModuleExpanded) Dot(name string) string {
|
func (n *graphNodeModuleExpanded) DotNode(name string, opts *GraphDotOpts) *dot.Node {
|
||||||
return fmt.Sprintf(
|
return dot.NewNode(name, map[string]string{
|
||||||
"\"%s\" [\n"+
|
"label": dag.VertexName(n.Original),
|
||||||
"\tlabel=\"%s\"\n"+
|
"shape": "component",
|
||||||
"\tshape=component\n"+
|
})
|
||||||
"];",
|
|
||||||
name,
|
|
||||||
dag.VertexName(n.Original))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GraphNodeEvalable impl.
|
// GraphNodeEvalable impl.
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
package terraform
|
package terraform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/dag"
|
"github.com/hashicorp/terraform/dag"
|
||||||
|
"github.com/hashicorp/terraform/dot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GraphNodeDotter can be implemented by a node to cause it to be included
|
// GraphNodeDotter can be implemented by a node to cause it to be included
|
||||||
|
@ -14,58 +12,174 @@ import (
|
||||||
// return a representation of this node.
|
// return a representation of this node.
|
||||||
type GraphNodeDotter interface {
|
type GraphNodeDotter interface {
|
||||||
// Dot is called to return the dot formatting for the node.
|
// Dot is called to return the dot formatting for the node.
|
||||||
// The parameter must be the title of the node.
|
// The first parameter is the title of the node.
|
||||||
Dot(string) string
|
// 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.
|
// GraphDotOpts are the options for generating a dot formatted Graph.
|
||||||
type GraphDotOpts struct{}
|
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
|
// GraphDot returns the dot formatting of a visual representation of
|
||||||
// the given Terraform graph.
|
// the given Terraform graph.
|
||||||
func GraphDot(g *Graph, opts *GraphDotOpts) string {
|
func GraphDot(g *Graph, opts *GraphDotOpts) (string, error) {
|
||||||
buf := new(bytes.Buffer)
|
dg := dot.NewGraph(map[string]string{
|
||||||
|
"compound": "true",
|
||||||
|
"newrank": "true",
|
||||||
|
})
|
||||||
|
dg.Directed = true
|
||||||
|
|
||||||
// Start the graph
|
err := graphDotSubgraph(dg, "root", g, opts, 0)
|
||||||
buf.WriteString("digraph {\n")
|
if err != nil {
|
||||||
buf.WriteString("\tcompound = true;\n")
|
return "", err
|
||||||
|
|
||||||
// Go through all the vertices and draw it
|
|
||||||
vertices := g.Vertices()
|
|
||||||
dotVertices := make(map[dag.Vertex]struct{}, len(vertices))
|
|
||||||
for _, v := range vertices {
|
|
||||||
if dn, ok := v.(GraphNodeDotter); !ok {
|
|
||||||
continue
|
|
||||||
} else if dn.Dot("fake") == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dotVertices[v] = struct{}{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for v, _ := range dotVertices {
|
return dg.String(), nil
|
||||||
dn := v.(GraphNodeDotter)
|
}
|
||||||
scanner := bufio.NewScanner(strings.NewReader(
|
|
||||||
dn.Dot(dag.VertexName(v))))
|
func graphDotSubgraph(
|
||||||
for scanner.Scan() {
|
dg *dot.Graph, modName string, g *Graph, opts *GraphDotOpts, modDepth int) error {
|
||||||
buf.WriteString("\t" + scanner.Text() + "\n")
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw all the edges
|
drawableVertices[v] = struct{}{}
|
||||||
for _, t := range g.DownEdges(v).List() {
|
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)
|
target := t.(dag.Vertex)
|
||||||
if _, ok := dotVertices[target]; !ok {
|
// Only want edges where both sides are drawable.
|
||||||
|
if _, ok := drawableVertices[target]; !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.WriteString(fmt.Sprintf(
|
if err := sg.AddEdgeBetween(
|
||||||
"\t\"%s\" -> \"%s\";\n",
|
graphDotNodeName(modName, v),
|
||||||
dag.VertexName(v),
|
graphDotNodeName(modName, target),
|
||||||
dag.VertexName(target)))
|
map[string]string{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// End the graph
|
// Recurse into any subgraphs
|
||||||
buf.WriteString("}\n")
|
for _, v := range toDraw {
|
||||||
return buf.String()
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,287 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/dot"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGraphDot(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Graph testGraphFunc
|
||||||
|
Opts GraphDotOpts
|
||||||
|
Expect string
|
||||||
|
Error string
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
Graph: func() *Graph { return &Graph{} },
|
||||||
|
Error: "No DOT origin nodes found",
|
||||||
|
},
|
||||||
|
"three-level": {
|
||||||
|
Graph: func() *Graph {
|
||||||
|
var g Graph
|
||||||
|
root := &testDrawableOrigin{"root"}
|
||||||
|
g.Add(root)
|
||||||
|
|
||||||
|
levelOne := []string{"foo", "bar"}
|
||||||
|
for _, s := range levelOne {
|
||||||
|
g.Add(&testDrawable{
|
||||||
|
VertexName: s,
|
||||||
|
DependentOnMock: []string{"root"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
levelTwo := []string{"baz", "qux"}
|
||||||
|
for i, s := range levelTwo {
|
||||||
|
g.Add(&testDrawable{
|
||||||
|
VertexName: s,
|
||||||
|
DependentOnMock: levelOne[i : i+1],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
g.ConnectDependents()
|
||||||
|
return &g
|
||||||
|
},
|
||||||
|
Expect: `
|
||||||
|
digraph {
|
||||||
|
compound = "true"
|
||||||
|
newrank = "true"
|
||||||
|
subgraph "root" {
|
||||||
|
"[root] bar"
|
||||||
|
"[root] baz"
|
||||||
|
"[root] foo"
|
||||||
|
"[root] qux"
|
||||||
|
"[root] root"
|
||||||
|
"[root] bar" -> "[root] root"
|
||||||
|
"[root] baz" -> "[root] foo"
|
||||||
|
"[root] foo" -> "[root] root"
|
||||||
|
"[root] qux" -> "[root] bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"cycle": {
|
||||||
|
Opts: GraphDotOpts{
|
||||||
|
DrawCycles: true,
|
||||||
|
},
|
||||||
|
Graph: func() *Graph {
|
||||||
|
var g Graph
|
||||||
|
root := &testDrawableOrigin{"root"}
|
||||||
|
g.Add(root)
|
||||||
|
|
||||||
|
g.Add(&testDrawable{
|
||||||
|
VertexName: "A",
|
||||||
|
DependentOnMock: []string{"root", "C"},
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Add(&testDrawable{
|
||||||
|
VertexName: "B",
|
||||||
|
DependentOnMock: []string{"A"},
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Add(&testDrawable{
|
||||||
|
VertexName: "C",
|
||||||
|
DependentOnMock: []string{"B"},
|
||||||
|
})
|
||||||
|
|
||||||
|
g.ConnectDependents()
|
||||||
|
return &g
|
||||||
|
},
|
||||||
|
Expect: `
|
||||||
|
digraph {
|
||||||
|
compound = "true"
|
||||||
|
newrank = "true"
|
||||||
|
subgraph "root" {
|
||||||
|
"[root] A"
|
||||||
|
"[root] B"
|
||||||
|
"[root] C"
|
||||||
|
"[root] root"
|
||||||
|
"[root] A" -> "[root] B" [color = "red", penwidth = "2.0"]
|
||||||
|
"[root] A" -> "[root] C"
|
||||||
|
"[root] A" -> "[root] root"
|
||||||
|
"[root] B" -> "[root] A"
|
||||||
|
"[root] B" -> "[root] C" [color = "red", penwidth = "2.0"]
|
||||||
|
"[root] C" -> "[root] A" [color = "red", penwidth = "2.0"]
|
||||||
|
"[root] C" -> "[root] B"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"subgraphs, no depth restriction": {
|
||||||
|
Opts: GraphDotOpts{
|
||||||
|
MaxDepth: -1,
|
||||||
|
},
|
||||||
|
Graph: func() *Graph {
|
||||||
|
var g Graph
|
||||||
|
root := &testDrawableOrigin{"root"}
|
||||||
|
g.Add(root)
|
||||||
|
|
||||||
|
var sub Graph
|
||||||
|
sub.Add(&testDrawableOrigin{"sub_root"})
|
||||||
|
|
||||||
|
var subsub Graph
|
||||||
|
subsub.Add(&testDrawableOrigin{"subsub_root"})
|
||||||
|
sub.Add(&testDrawableSubgraph{
|
||||||
|
VertexName: "subsub",
|
||||||
|
SubgraphMock: &subsub,
|
||||||
|
DependentOnMock: []string{"sub_root"},
|
||||||
|
})
|
||||||
|
g.Add(&testDrawableSubgraph{
|
||||||
|
VertexName: "sub",
|
||||||
|
SubgraphMock: &sub,
|
||||||
|
DependentOnMock: []string{"root"},
|
||||||
|
})
|
||||||
|
|
||||||
|
g.ConnectDependents()
|
||||||
|
sub.ConnectDependents()
|
||||||
|
return &g
|
||||||
|
},
|
||||||
|
Expect: `
|
||||||
|
digraph {
|
||||||
|
compound = "true"
|
||||||
|
newrank = "true"
|
||||||
|
subgraph "root" {
|
||||||
|
"[root] root"
|
||||||
|
"[root] sub"
|
||||||
|
"[root] sub" -> "[root] root"
|
||||||
|
}
|
||||||
|
subgraph "cluster_sub" {
|
||||||
|
label = "sub"
|
||||||
|
"[sub] sub_root"
|
||||||
|
"[sub] subsub"
|
||||||
|
"[sub] subsub" -> "[sub] sub_root"
|
||||||
|
}
|
||||||
|
subgraph "cluster_subsub" {
|
||||||
|
label = "subsub"
|
||||||
|
"[subsub] subsub_root"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"subgraphs, with depth restriction": {
|
||||||
|
Opts: GraphDotOpts{
|
||||||
|
MaxDepth: 1,
|
||||||
|
},
|
||||||
|
Graph: func() *Graph {
|
||||||
|
var g Graph
|
||||||
|
root := &testDrawableOrigin{"root"}
|
||||||
|
g.Add(root)
|
||||||
|
|
||||||
|
var sub Graph
|
||||||
|
sub.Add(&testDrawableOrigin{"sub_root"})
|
||||||
|
|
||||||
|
var subsub Graph
|
||||||
|
subsub.Add(&testDrawableOrigin{"subsub_root"})
|
||||||
|
sub.Add(&testDrawableSubgraph{
|
||||||
|
VertexName: "subsub",
|
||||||
|
SubgraphMock: &subsub,
|
||||||
|
DependentOnMock: []string{"sub_root"},
|
||||||
|
})
|
||||||
|
g.Add(&testDrawableSubgraph{
|
||||||
|
VertexName: "sub",
|
||||||
|
SubgraphMock: &sub,
|
||||||
|
DependentOnMock: []string{"root"},
|
||||||
|
})
|
||||||
|
|
||||||
|
g.ConnectDependents()
|
||||||
|
sub.ConnectDependents()
|
||||||
|
return &g
|
||||||
|
},
|
||||||
|
Expect: `
|
||||||
|
digraph {
|
||||||
|
compound = "true"
|
||||||
|
newrank = "true"
|
||||||
|
subgraph "root" {
|
||||||
|
"[root] root"
|
||||||
|
"[root] sub"
|
||||||
|
"[root] sub" -> "[root] root"
|
||||||
|
}
|
||||||
|
subgraph "cluster_sub" {
|
||||||
|
label = "sub"
|
||||||
|
"[sub] sub_root"
|
||||||
|
"[sub] subsub"
|
||||||
|
"[sub] subsub" -> "[sub] sub_root"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for tn, tc := range cases {
|
||||||
|
actual, err := GraphDot(tc.Graph(), &tc.Opts)
|
||||||
|
if (err == nil) && tc.Error != "" {
|
||||||
|
t.Fatalf("%s: expected err: %s, got none", tn, tc.Error)
|
||||||
|
}
|
||||||
|
if (err != nil) && (tc.Error == "") {
|
||||||
|
t.Fatalf("%s: unexpected err: %s", tn, err)
|
||||||
|
}
|
||||||
|
if (err != nil) && (tc.Error != "") {
|
||||||
|
if !strings.Contains(err.Error(), tc.Error) {
|
||||||
|
t.Fatalf("%s: expected err: %s\nto contain: %s", tn, err, tc.Error)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := strings.TrimSpace(tc.Expect) + "\n"
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("%s:\n\nexpected:\n%s\n\ngot:\n%s", tn, expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testGraphFunc func() *Graph
|
||||||
|
|
||||||
|
type testDrawable struct {
|
||||||
|
VertexName string
|
||||||
|
DependentOnMock []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *testDrawable) Name() string {
|
||||||
|
return node.VertexName
|
||||||
|
}
|
||||||
|
func (node *testDrawable) DotNode(n string, opts *GraphDotOpts) *dot.Node {
|
||||||
|
return dot.NewNode(n, map[string]string{})
|
||||||
|
}
|
||||||
|
func (node *testDrawable) DependableName() []string {
|
||||||
|
return []string{node.VertexName}
|
||||||
|
}
|
||||||
|
func (node *testDrawable) DependentOn() []string {
|
||||||
|
return node.DependentOnMock
|
||||||
|
}
|
||||||
|
|
||||||
|
type testDrawableOrigin struct {
|
||||||
|
VertexName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *testDrawableOrigin) Name() string {
|
||||||
|
return node.VertexName
|
||||||
|
}
|
||||||
|
func (node *testDrawableOrigin) DotNode(n string, opts *GraphDotOpts) *dot.Node {
|
||||||
|
return dot.NewNode(n, map[string]string{})
|
||||||
|
}
|
||||||
|
func (node *testDrawableOrigin) DotOrigin() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (node *testDrawableOrigin) DependableName() []string {
|
||||||
|
return []string{node.VertexName}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testDrawableSubgraph struct {
|
||||||
|
VertexName string
|
||||||
|
SubgraphMock *Graph
|
||||||
|
DependentOnMock []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *testDrawableSubgraph) Name() string {
|
||||||
|
return node.VertexName
|
||||||
|
}
|
||||||
|
func (node *testDrawableSubgraph) Subgraph() *Graph {
|
||||||
|
return node.SubgraphMock
|
||||||
|
}
|
||||||
|
func (node *testDrawableSubgraph) DotNode(n string, opts *GraphDotOpts) *dot.Node {
|
||||||
|
return dot.NewNode(n, map[string]string{})
|
||||||
|
}
|
||||||
|
func (node *testDrawableSubgraph) DependentOn() []string {
|
||||||
|
return node.DependentOnMock
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
"github.com/hashicorp/terraform/dag"
|
"github.com/hashicorp/terraform/dag"
|
||||||
|
"github.com/hashicorp/terraform/dot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GraphNodeProvider is an interface that nodes that can be a provider
|
// GraphNodeProvider is an interface that nodes that can be a provider
|
||||||
|
@ -176,6 +177,19 @@ func (n *graphNodeDisabledProvider) Name() string {
|
||||||
return fmt.Sprintf("%s (disabled)", dag.VertexName(n.GraphNodeProvider))
|
return fmt.Sprintf("%s (disabled)", dag.VertexName(n.GraphNodeProvider))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphNodeDotter impl.
|
||||||
|
func (n *graphNodeDisabledProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node {
|
||||||
|
return dot.NewNode(name, map[string]string{
|
||||||
|
"label": n.Name(),
|
||||||
|
"shape": "diamond",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphNodeDotterOrigin impl.
|
||||||
|
func (n *graphNodeDisabledProvider) DotOrigin() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
type graphNodeMissingProvider struct {
|
type graphNodeMissingProvider struct {
|
||||||
ProviderNameValue string
|
ProviderNameValue string
|
||||||
}
|
}
|
||||||
|
@ -198,14 +212,16 @@ func (n *graphNodeMissingProvider) ProviderConfig() *config.RawConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GraphNodeDotter impl.
|
// GraphNodeDotter impl.
|
||||||
func (n *graphNodeMissingProvider) Dot(name string) string {
|
func (n *graphNodeMissingProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node {
|
||||||
return fmt.Sprintf(
|
return dot.NewNode(name, map[string]string{
|
||||||
"\"%s\" [\n"+
|
"label": n.Name(),
|
||||||
"\tlabel=\"%s\"\n"+
|
"shape": "diamond",
|
||||||
"\tshape=diamond\n"+
|
})
|
||||||
"];",
|
}
|
||||||
name,
|
|
||||||
n.Name())
|
// GraphNodeDotterOrigin impl.
|
||||||
|
func (n *graphNodeMissingProvider) DotOrigin() bool {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func providerVertexMap(g *Graph) map[string]dag.Vertex {
|
func providerVertexMap(g *Graph) map[string]dag.Vertex {
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
package terraform
|
package terraform
|
||||||
|
|
||||||
import (
|
import "github.com/hashicorp/terraform/dag"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/dag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RootTransformer is a GraphTransformer that adds a root to the graph.
|
// RootTransformer is a GraphTransformer that adds a root to the graph.
|
||||||
type RootTransformer struct{}
|
type RootTransformer struct{}
|
||||||
|
@ -38,7 +34,3 @@ type graphNodeRoot struct{}
|
||||||
func (n graphNodeRoot) Name() string {
|
func (n graphNodeRoot) Name() string {
|
||||||
return "root"
|
return "root"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n graphNodeRoot) Dot(name string) string {
|
|
||||||
return fmt.Sprintf("\"%s\" [shape=circle];", name)
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,18 +16,26 @@ The output is in the DOT format, which can be used by
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Usage: `terraform graph [options] PATH`
|
Usage: `terraform graph [options] [DIR]`
|
||||||
|
|
||||||
Outputs the visual graph of Terraform resources. If the path given is
|
Outputs the visual dependency graph of Terraform resources according to
|
||||||
the path to a configuration, the dependency graph of the resources are
|
configuration files in DIR (or the current directory if omitted).
|
||||||
shown. If the path is a plan file, then the dependency graph of the
|
|
||||||
plan itself is shown.
|
The graph is outputted in DOT format. The typical program that can
|
||||||
|
read this format is GraphViz, but many web services are also available
|
||||||
|
to read this format.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
|
* `-draw-cycles` - Highlight any cycles in the graph with colored edges.
|
||||||
|
This helps when diagnosing cycle errors.
|
||||||
|
|
||||||
* `-module-depth=n` - The maximum depth to expand modules. By default this is
|
* `-module-depth=n` - The maximum depth to expand modules. By default this is
|
||||||
zero, which will not expand modules at all.
|
zero, which will not expand modules at all.
|
||||||
|
|
||||||
|
* `-verbose` - Generate a verbose, "worst-case" graph, with all nodes
|
||||||
|
for potential operations in place.
|
||||||
|
|
||||||
## Generating Images
|
## Generating Images
|
||||||
|
|
||||||
The output of `terraform graph` is in the DOT format, which can
|
The output of `terraform graph` is in the DOT format, which can
|
||||||
|
|
Loading…
Reference in New Issue