diff --git a/command/graph.go b/command/graph.go index 6d698de0a..834d6c865 100644 --- a/command/graph.go +++ b/command/graph.go @@ -5,6 +5,8 @@ import ( "fmt" "os" "strings" + + "github.com/hashicorp/terraform/terraform" ) // GraphCommand is a Command implementation that takes a Terraform @@ -56,7 +58,7 @@ func (c *GraphCommand) Run(args []string) int { return 1 } - c.Ui.Output(g.String()) + c.Ui.Output(terraform.GraphDot(g, nil)) return 0 } diff --git a/terraform/graph_config_node.go b/terraform/graph_config_node.go index 5ee982512..30a614146 100644 --- a/terraform/graph_config_node.go +++ b/terraform/graph_config_node.go @@ -173,6 +173,17 @@ func (n *GraphNodeConfigProvider) ProviderName() string { return n.Provider.Name } +// GraphNodeDotter impl. +func (n *GraphNodeConfigProvider) Dot(name string) string { + return fmt.Sprintf( + "\"%s\" [\n"+ + "\tlabel=\"%s\"\n"+ + "\tshape=diamond\n"+ + "];", + name, + n.Name()) +} + // GraphNodeConfigResource represents a resource within the config graph. type GraphNodeConfigResource struct { Resource *config.Resource @@ -235,6 +246,21 @@ func (n *GraphNodeConfigResource) Name() string { return result } +// GraphNodeDotter impl. +func (n *GraphNodeConfigResource) Dot(name string) string { + if n.DestroyMode != DestroyNone { + return "" + } + + return fmt.Sprintf( + "\"%s\" [\n"+ + "\tlabel=\"%s\"\n"+ + "\tshape=box\n"+ + "];", + name, + n.Name()) +} + // GraphNodeDynamicExpandable impl. func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error) { state, lock := ctx.State() @@ -476,6 +502,17 @@ func (n *graphNodeModuleExpanded) Name() string { return fmt.Sprintf("%s (expanded)", dag.VertexName(n.Original)) } +// GraphNodeDotter impl. +func (n *graphNodeModuleExpanded) Dot(name string) string { + return fmt.Sprintf( + "\"%s\" [\n"+ + "\tlabel=\"%s\"\n"+ + "\tshape=component\n"+ + "];", + name, + dag.VertexName(n.Original)) +} + // GraphNodeEvalable impl. func (n *graphNodeModuleExpanded) EvalTree() EvalNode { var resourceConfig *ResourceConfig diff --git a/terraform/graph_dot.go b/terraform/graph_dot.go new file mode 100644 index 000000000..2e420c637 --- /dev/null +++ b/terraform/graph_dot.go @@ -0,0 +1,71 @@ +package terraform + +import ( + "bufio" + "bytes" + "fmt" + "strings" + + "github.com/hashicorp/terraform/dag" +) + +// 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 parameter must be the title of the node. + Dot(string) string +} + +// GraphDotOpts are the options for generating a dot formatted Graph. +type GraphDotOpts struct{} + +// GraphDot returns the dot formatting of a visual representation of +// the given Terraform graph. +func GraphDot(g *Graph, opts *GraphDotOpts) string { + buf := new(bytes.Buffer) + + // Start the graph + buf.WriteString("digraph {\n") + buf.WriteString("\tcompound = true;\n") + + // 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 { + dn := v.(GraphNodeDotter) + scanner := bufio.NewScanner(strings.NewReader( + dn.Dot(dag.VertexName(v)))) + for scanner.Scan() { + buf.WriteString("\t" + scanner.Text() + "\n") + } + + // Draw all the edges + for _, t := range g.DownEdges(v).List() { + target := t.(dag.Vertex) + if _, ok := dotVertices[target]; !ok { + continue + } + + buf.WriteString(fmt.Sprintf( + "\t\"%s\" -> \"%s\";\n", + dag.VertexName(v), + dag.VertexName(target))) + } + } + + // End the graph + buf.WriteString("}\n") + return buf.String() +} diff --git a/terraform/transform_provider.go b/terraform/transform_provider.go index 3d19b80eb..f6c566b26 100644 --- a/terraform/transform_provider.go +++ b/terraform/transform_provider.go @@ -111,6 +111,17 @@ func (n *graphNodeMissingProvider) ProviderName() string { return n.ProviderNameValue } +// GraphNodeDotter impl. +func (n *graphNodeMissingProvider) Dot(name string) string { + return fmt.Sprintf( + "\"%s\" [\n"+ + "\tlabel=\"%s\"\n"+ + "\tshape=diamond\n"+ + "];", + name, + n.Name()) +} + func providerVertexMap(g *Graph) map[string]dag.Vertex { m := make(map[string]dag.Vertex) for _, v := range g.Vertices() { diff --git a/terraform/transform_root.go b/terraform/transform_root.go index be75e7302..bf5640ac6 100644 --- a/terraform/transform_root.go +++ b/terraform/transform_root.go @@ -1,6 +1,8 @@ package terraform import ( + "fmt" + "github.com/hashicorp/terraform/dag" ) @@ -36,3 +38,7 @@ type graphNodeRoot struct{} func (n graphNodeRoot) Name() string { return "root" } + +func (n graphNodeRoot) Dot(name string) string { + return fmt.Sprintf("\"%s\" [shape=circle];", name) +}