Skip to content

Commit

Permalink
core: graph command gets -verbose and -draw-cycles
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
phinze committed Apr 27, 2015
1 parent d4b9362 commit ce49dd6
Show file tree
Hide file tree
Showing 10 changed files with 875 additions and 130 deletions.
35 changes: 27 additions & 8 deletions command/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ type GraphCommand struct {

func (c *GraphCommand) Run(args []string) int {
var moduleDepth int
var verbose bool
var drawCycles bool

args = c.Meta.process(args, false)

cmdFlags := flag.NewFlagSet("graph", flag.ContinueOnError)
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()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
Expand Down Expand Up @@ -52,38 +56,53 @@ func (c *GraphCommand) Run(args []string) int {
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{
Validate: true,
Verbose: false,
Verbose: verbose,
Validate: false,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Error creating graph: %s", err))
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
}

func (c *GraphCommand) Help() string {
helpText := `
Usage: terraform graph [options] PATH
Usage: terraform graph [options] [DIR]
Outputs the visual graph of Terraform resources. If the path given is
the path to a configuration, the dependency graph of the resources are
shown. If the path is a plan file, then the dependency graph of the
plan itself is shown.
Outputs the visual dependency graph of Terraform resources according to
configuration files in DIR (or the current directory if omitted).
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:
-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
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)
}
Expand Down
118 changes: 80 additions & 38 deletions dag/dag.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dag

import (
"fmt"
"sort"
"strings"
"sync"

Expand All @@ -17,17 +18,21 @@ type AcyclicGraph struct {
// WalkFunc is the callback used for walking the graph.
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
// provided starting Vertex v.
func (g *AcyclicGraph) Ancestors(v Vertex) (*Set, error) {
s := new(Set)
start := asVertexList(g.DownEdges(v))
memoFunc := func(v Vertex) error {
start := AsVertexList(g.DownEdges(v))
memoFunc := func(v Vertex, d int) error {
s.Add(v)
return nil
}

if err := g.depthFirstWalk(start, memoFunc); err != nil {
if err := g.DepthFirstWalk(start, memoFunc); err != nil {
return nil, err
}

Expand All @@ -38,13 +43,13 @@ func (g *AcyclicGraph) Ancestors(v Vertex) (*Set, error) {
// provided starting Vertex v.
func (g *AcyclicGraph) Descendents(v Vertex) (*Set, error) {
s := new(Set)
start := asVertexList(g.UpEdges(v))
memoFunc := func(v Vertex) error {
start := AsVertexList(g.UpEdges(v))
memoFunc := func(v Vertex, d int) error {
s.Add(v)
return nil
}

if err := g.reverseDepthFirstWalk(start, memoFunc); err != nil {
if err := g.ReverseDepthFirstWalk(start, memoFunc); err != nil {
return nil, err
}

Expand Down Expand Up @@ -92,14 +97,13 @@ func (g *AcyclicGraph) TransitiveReduction() {
// 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 _, u := range g.Vertices() {
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))
for _, vPrime := range asVertexList(shared) {
for _, vPrime := range AsVertexList(shared) {
g.RemoveEdge(BasicEdge(u, vPrime))
}

Expand All @@ -117,12 +121,7 @@ func (g *AcyclicGraph) Validate() error {

// Look for cycles of more than 1 component
var err error
var cycles [][]Vertex
for _, cycle := range StronglyConnected(&g.Graph) {
if len(cycle) > 1 {
cycles = append(cycles, cycle)
}
}
cycles := g.Cycles()
if len(cycles) > 0 {
for _, cycle := range cycles {
cycleStr := make([]string, len(cycle))
Expand All @@ -146,6 +145,16 @@ func (g *AcyclicGraph) Validate() error {
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.
// This will walk nodes in parallel if it can. Because the walk is done
// in parallel, the error returned will be a multierror.
Expand Down Expand Up @@ -175,7 +184,7 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error {
for _, v := range vertices {
// Build our list of dependencies and the list of channels to
// 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))
for i, dep := range deps {
depChs[i] = vertMap[dep]
Expand Down Expand Up @@ -229,7 +238,7 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error {
}

// simple convenience helper for converting a dag.Set to a []Vertex
func asVertexList(s *Set) []Vertex {
func AsVertexList(s *Set) []Vertex {
rawList := s.List()
vertexList := make([]Vertex, len(rawList))
for i, raw := range rawList {
Expand All @@ -238,34 +247,48 @@ func asVertexList(s *Set) []Vertex {
return vertexList
}

type vertexAtDepth struct {
Vertex Vertex
Depth int
}

// 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
// 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{})
frontier := make([]Vertex, len(start))
copy(frontier, start)
frontier := make([]*vertexAtDepth, len(start))
for i, v := range start {
frontier[i] = &vertexAtDepth{
Vertex: v,
Depth: 0,
}
}
for len(frontier) > 0 {
// Pop the current vertex
n := len(frontier)
current := frontier[n-1]
frontier = frontier[:n-1]

// Check if we've seen this already and return...
if _, ok := seen[current]; ok {
if _, ok := seen[current.Vertex]; ok {
continue
}
seen[current] = struct{}{}
seen[current.Vertex] = struct{}{}

// Visit the current node
if err := cb(current); err != nil {
if err := f(current.Vertex, current.Depth); err != nil {
return err
}

// Visit targets of this in reverse order.
targets := g.DownEdges(current).List()
for i := len(targets) - 1; i >= 0; i-- {
frontier = append(frontier, targets[i].(Vertex))
// Visit targets of this in a consistent order.
targets := AsVertexList(g.DownEdges(current.Vertex))
sort.Sort(byVertexName(targets))
for _, t := range targets {
frontier = append(frontier, &vertexAtDepth{
Vertex: t,
Depth: current.Depth + 1,
})
}
}

Expand All @@ -274,33 +297,52 @@ func (g *AcyclicGraph) depthFirstWalk(start []Vertex, cb WalkFunc) error {

// reverseDepthFirstWalk does a depth-first walk _up_ the graph starting from
// 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{})
frontier := make([]Vertex, len(start))
copy(frontier, start)
frontier := make([]*vertexAtDepth, len(start))
for i, v := range start {
frontier[i] = &vertexAtDepth{
Vertex: v,
Depth: 0,
}
}
for len(frontier) > 0 {
// Pop the current vertex
n := len(frontier)
current := frontier[n-1]
frontier = frontier[:n-1]

// Check if we've seen this already and return...
if _, ok := seen[current]; ok {
if _, ok := seen[current.Vertex]; ok {
continue
}
seen[current] = struct{}{}
seen[current.Vertex] = struct{}{}

// Visit the current node
if err := cb(current); err != nil {
if err := f(current.Vertex, current.Depth); err != nil {
return err
}

// Visit targets of this in reverse order.
targets := g.UpEdges(current).List()
for i := len(targets) - 1; i >= 0; i-- {
frontier = append(frontier, targets[i].(Vertex))
// Visit targets of this in a consistent order.
targets := AsVertexList(g.UpEdges(current.Vertex))
sort.Sort(byVertexName(targets))
for _, t := range targets {
frontier = append(frontier, &vertexAtDepth{
Vertex: t,
Depth: current.Depth + 1,
})
}
}

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])
}
Loading

0 comments on commit ce49dd6

Please sign in to comment.