Skip to content

Commit

Permalink
Merge pull request #20 from hashicorp/f-graph-improvements
Browse files Browse the repository at this point in the history
Improved Graph Output
  • Loading branch information
mitchellh committed Jul 14, 2014
2 parents ac0168d + 7e60a20 commit ca7148c
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 55 deletions.
2 changes: 1 addition & 1 deletion command/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (c *ApplyCommand) Run(args []string) int {
}

// Build the context based on the arguments given
ctx, err := c.Context(configPath, planStatePath)
ctx, err := c.Context(configPath, planStatePath, true)
if err != nil {
c.Ui.Error(err.Error())
return 1
Expand Down
14 changes: 3 additions & 11 deletions command/graph.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package command

import (
"bytes"
"flag"
"fmt"
"os"
"strings"

"github.com/hashicorp/terraform/digraph"
"github.com/hashicorp/terraform/terraform"
)

// GraphCommand is a Command implementation that takes a Terraform
Expand Down Expand Up @@ -41,7 +40,7 @@ func (c *GraphCommand) Run(args []string) int {
}
}

ctx, err := c.Context(path, "")
ctx, err := c.Context(path, "", false)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err))
return 1
Expand All @@ -53,14 +52,7 @@ func (c *GraphCommand) Run(args []string) int {
return 1
}

buf := new(bytes.Buffer)
nodes := make([]digraph.Node, len(g.Nouns))
for i, n := range g.Nouns {
nodes[i] = n
}
digraph.GenerateDot(nodes, buf)

c.Ui.Output(buf.String())
c.Ui.Output(terraform.GraphDot(g))

return 0
}
Expand Down
8 changes: 5 additions & 3 deletions command/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (m *Meta) Colorize() *colorstring.Colorize {

// Context returns a Terraform Context taking into account the context
// options used to initialize this meta configuration.
func (m *Meta) Context(path, statePath string) (*terraform.Context, error) {
func (m *Meta) Context(path, statePath string, doPlan bool) (*terraform.Context, error) {
opts := m.contextOpts()

// First try to just read the plan directly from the path given.
Expand Down Expand Up @@ -84,8 +84,10 @@ func (m *Meta) Context(path, statePath string) (*terraform.Context, error) {
opts.State = state
ctx := terraform.NewContext(opts)

if _, err := ctx.Plan(nil); err != nil {
return nil, fmt.Errorf("Error running plan: %s", err)
if doPlan {
if _, err := ctx.Plan(nil); err != nil {
return nil, fmt.Errorf("Error running plan: %s", err)
}
}

return ctx, nil
Expand Down
2 changes: 1 addition & 1 deletion command/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (c *PlanCommand) Run(args []string) int {
}
}

ctx, err := c.Context(path, statePath)
ctx, err := c.Context(path, statePath, false)
if err != nil {
c.Ui.Error(err.Error())
return 1
Expand Down
2 changes: 1 addition & 1 deletion command/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (c *RefreshCommand) Run(args []string) int {
}

// Build the context based on the arguments given
ctx, err := c.Context(configPath, statePath)
ctx, err := c.Context(configPath, statePath, false)
if err != nil {
c.Ui.Error(err.Error())
return 1
Expand Down
12 changes: 9 additions & 3 deletions digraph/graphviz.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@ import (
"io"
)

// GenerateDot is used to emit a GraphViz compatible definition
// WriteDot is used to emit a GraphViz compatible definition
// for a directed graph. It can be used to dump a .dot file.
func GenerateDot(nodes []Node, w io.Writer) {
func WriteDot(w io.Writer, nodes []Node) error {
w.Write([]byte("digraph {\n"))
defer w.Write([]byte("}\n"))

for _, n := range nodes {
w.Write([]byte(fmt.Sprintf("\t\"%s\";\n", n)))
nodeLine := fmt.Sprintf("\t\"%s\";\n", n)

w.Write([]byte(nodeLine))

for _, edge := range n.Edges() {
target := edge.Tail()
line := fmt.Sprintf("\t\"%s\" -> \"%s\" [label=\"%s\"];\n",
n, target, edge)
w.Write([]byte(line))
}
}

return nil
}
57 changes: 22 additions & 35 deletions digraph/graphviz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"testing"
)

func Test_GenerateDot(t *testing.T) {
func TestWriteDot(t *testing.T) {
nodes := ParseBasic(`a -> b ; foo
a -> c
b -> d
Expand All @@ -18,40 +18,27 @@ b -> e
}

buf := bytes.NewBuffer(nil)
GenerateDot(nlist, buf)

out := string(buf.Bytes())
if !strings.HasPrefix(out, "digraph {\n") {
t.Fatalf("bad: %v", out)
}
if !strings.HasSuffix(out, "\n}\n") {
t.Fatalf("bad: %v", out)
}
if !strings.Contains(out, "\n\t\"a\";\n") {
t.Fatalf("bad: %v", out)
}
if !strings.Contains(out, "\n\t\"b\";\n") {
t.Fatalf("bad: %v", out)
}
if !strings.Contains(out, "\n\t\"c\";\n") {
t.Fatalf("bad: %v", out)
}
if !strings.Contains(out, "\n\t\"d\";\n") {
t.Fatalf("bad: %v", out)
}
if !strings.Contains(out, "\n\t\"e\";\n") {
t.Fatalf("bad: %v", out)
if err := WriteDot(buf, nlist); err != nil {
t.Fatalf("err: %s", err)
}
if !strings.Contains(out, "\n\t\"a\" -> \"b\" [label=\"foo\"];\n") {
t.Fatalf("bad: %v", out)
}
if !strings.Contains(out, "\n\t\"a\" -> \"c\" [label=\"Edge\"];\n") {
t.Fatalf("bad: %v", out)
}
if !strings.Contains(out, "\n\t\"b\" -> \"d\" [label=\"Edge\"];\n") {
t.Fatalf("bad: %v", out)
}
if !strings.Contains(out, "\n\t\"b\" -> \"e\" [label=\"Edge\"];\n") {
t.Fatalf("bad: %v", out)

actual := strings.TrimSpace(string(buf.Bytes()))
expected := strings.TrimSpace(writeDotStr)
if actual != expected {
t.Fatalf("bad: %s", actual)
}
}

const writeDotStr = `
digraph {
"a";
"a" -> "b" [label="foo"];
"a" -> "c" [label="Edge"];
"b";
"b" -> "d" [label="Edge"];
"b" -> "e" [label="Edge"];
"c";
"d";
"e";
}
`
200 changes: 200 additions & 0 deletions terraform/graph_dot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package terraform

import (
"bytes"
"fmt"

"github.com/hashicorp/terraform/depgraph"
)

// GraphDot returns the dot formatting of a visual representation of
// the given Terraform graph.
func GraphDot(g *depgraph.Graph) string {
buf := new(bytes.Buffer)
buf.WriteString("digraph {\n")

// Determine and add the title
// graphDotTitle(buf, g)

// Add all the resource.
graphDotAddResources(buf, g)

// Add all the resource providers
graphDotAddResourceProviders(buf, g)

buf.WriteString("}\n")
return buf.String()
}

func graphDotAddRoot(buf *bytes.Buffer, n *depgraph.Noun) {
buf.WriteString(fmt.Sprintf("\t\"%s\" [shape=circle];\n", "root"))

for _, e := range n.Edges() {
target := e.Tail()
buf.WriteString(fmt.Sprintf(
"\t\"%s\" -> \"%s\";\n",
"root",
target))
}
}

func graphDotAddResources(buf *bytes.Buffer, g *depgraph.Graph) {
// Determine if we have diffs. If we do, then we're graphing a
// plan, which alters our graph a bit.
hasDiff := false
for _, n := range g.Nouns {
rn, ok := n.Meta.(*GraphNodeResource)
if !ok {
continue
}
if rn.Resource.Diff != nil && !rn.Resource.Diff.Empty() {
hasDiff = true
break
}
}

var edgeBuf bytes.Buffer
// Do all the non-destroy resources
buf.WriteString("\tsubgraph {\n")
for _, n := range g.Nouns {
rn, ok := n.Meta.(*GraphNodeResource)
if !ok {
continue
}
if rn.Resource.Diff != nil && rn.Resource.Diff.Destroy {
continue
}

// If we have diffs then we're graphing a plan. If we don't have
// have a diff on this resource, don't graph anything, since the
// plan wouldn't do anything to this resource.
if hasDiff {
if rn.Resource.Diff == nil || rn.Resource.Diff.Empty() {
continue
}
}

// Determine the colors. White = no change, yellow = change,
// green = create. Destroy is in the next section.
var color, fillColor string
if rn.Resource.Diff != nil && !rn.Resource.Diff.Empty() {
if rn.Resource.State != nil && rn.Resource.State.ID != "" {
color = "#FFFF00"
fillColor = "#FFFF94"
} else {
color = "#00FF00"
fillColor = "#9EFF9E"
}
}

// Create this node.
buf.WriteString(fmt.Sprintf("\t\t\"%s\" [\n", n))
buf.WriteString("\t\t\tshape=box\n")
if color != "" {
buf.WriteString("\t\t\tstyle=filled\n")
buf.WriteString(fmt.Sprintf("\t\t\tcolor=\"%s\"\n", color))
buf.WriteString(fmt.Sprintf("\t\t\tfillcolor=\"%s\"\n", fillColor))
}
buf.WriteString("\t\t];\n")

// Build up all the edges in a separate buffer so they're not in the
// subgraph.
for _, e := range n.Edges() {
target := e.Tail()
edgeBuf.WriteString(fmt.Sprintf(
"\t\"%s\" -> \"%s\";\n",
n,
target))
}
}
buf.WriteString("\t}\n\n")
if edgeBuf.Len() > 0 {
buf.WriteString(edgeBuf.String())
buf.WriteString("\n")
}

// Do all the destroy resources
edgeBuf.Reset()
buf.WriteString("\tsubgraph {\n")
for _, n := range g.Nouns {
rn, ok := n.Meta.(*GraphNodeResource)
if !ok {
continue
}
if rn.Resource.Diff == nil || !rn.Resource.Diff.Destroy {
continue
}

buf.WriteString(fmt.Sprintf(
"\t\t\"%s\" [shape=box,style=filled,color=\"#FF0000\",fillcolor=\"#FF9494\"];\n", n))

for _, e := range n.Edges() {
target := e.Tail()
edgeBuf.WriteString(fmt.Sprintf(
"\t\"%s\" -> \"%s\";\n",
n,
target))
}
}
buf.WriteString("\t}\n\n")
if edgeBuf.Len() > 0 {
buf.WriteString(edgeBuf.String())
buf.WriteString("\n")
}
}

func graphDotAddResourceProviders(buf *bytes.Buffer, g *depgraph.Graph) {
var edgeBuf bytes.Buffer
buf.WriteString("\tsubgraph {\n")
for _, n := range g.Nouns {
_, ok := n.Meta.(*GraphNodeResourceProvider)
if !ok {
continue
}

// Create this node.
buf.WriteString(fmt.Sprintf("\t\t\"%s\" [\n", n))
buf.WriteString("\t\t\tshape=diamond\n")
buf.WriteString("\t\t];\n")

// Build up all the edges in a separate buffer so they're not in the
// subgraph.
for _, e := range n.Edges() {
target := e.Tail()
edgeBuf.WriteString(fmt.Sprintf(
"\t\"%s\" -> \"%s\";\n",
n,
target))
}
}
buf.WriteString("\t}\n\n")
if edgeBuf.Len() > 0 {
buf.WriteString(edgeBuf.String())
buf.WriteString("\n")
}
}

func graphDotTitle(buf *bytes.Buffer, g *depgraph.Graph) {
// Determine if we have diffs. If we do, then we're graphing a
// plan, which alters our graph a bit.
hasDiff := false
for _, n := range g.Nouns {
rn, ok := n.Meta.(*GraphNodeResource)
if !ok {
continue
}
if rn.Resource.Diff != nil && !rn.Resource.Diff.Empty() {
hasDiff = true
break
}
}

graphType := "Configuration"
if hasDiff {
graphType = "Plan"
}
title := fmt.Sprintf("Terraform %s Resource Graph", graphType)

buf.WriteString(fmt.Sprintf("\tlabel=\"%s\\n\\n\\n\";\n", title))
buf.WriteString("\tlabelloc=\"t\";\n\n")
}

0 comments on commit ca7148c

Please sign in to comment.