From a63125b54ffb3ef6a10c5eb27f9fd1096838dee3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 14 Jul 2014 09:00:01 -0700 Subject: [PATCH 1/4] digraph: more idiomatic writedot api --- digraph/graphviz.go | 10 +++++-- digraph/graphviz_test.go | 57 ++++++++++++++++------------------------ 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/digraph/graphviz.go b/digraph/graphviz.go index 2908dfcd8579..cf0c4912f7c5 100644 --- a/digraph/graphviz.go +++ b/digraph/graphviz.go @@ -7,11 +7,15 @@ import ( // GenerateDot 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", @@ -19,4 +23,6 @@ func GenerateDot(nodes []Node, w io.Writer) { w.Write([]byte(line)) } } + + return nil } diff --git a/digraph/graphviz_test.go b/digraph/graphviz_test.go index 17bcb6c7e8a7..fce1ebb6f503 100644 --- a/digraph/graphviz_test.go +++ b/digraph/graphviz_test.go @@ -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 @@ -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"; +} +` From ad3c0593a358aab9a1a814870260f0c9e4f0c9a5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 14 Jul 2014 11:34:52 -0700 Subject: [PATCH 2/4] terraform: GraphDot --- command/graph.go | 12 +-- terraform/graph_dot.go | 200 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+), 10 deletions(-) create mode 100644 terraform/graph_dot.go diff --git a/command/graph.go b/command/graph.go index f0c4bcf3fac6..42e87da498d8 100644 --- a/command/graph.go +++ b/command/graph.go @@ -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 @@ -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 } diff --git a/terraform/graph_dot.go b/terraform/graph_dot.go new file mode 100644 index 000000000000..66057dd7dfdc --- /dev/null +++ b/terraform/graph_dot.go @@ -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") +} From 6c8c09c784eed77bfcb75f49d03a4b4938eee6ad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 14 Jul 2014 11:48:03 -0700 Subject: [PATCH 3/4] command/*: only Plan on the Apply --- command/apply.go | 2 +- command/graph.go | 2 +- command/meta.go | 8 +++++--- command/plan.go | 2 +- command/refresh.go | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/command/apply.go b/command/apply.go index 0276d984755e..0ca22b73f9f9 100644 --- a/command/apply.go +++ b/command/apply.go @@ -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 diff --git a/command/graph.go b/command/graph.go index 42e87da498d8..68c8bddbf718 100644 --- a/command/graph.go +++ b/command/graph.go @@ -40,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 diff --git a/command/meta.go b/command/meta.go index 3bafdb7a8445..d8ee72293176 100644 --- a/command/meta.go +++ b/command/meta.go @@ -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. @@ -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 diff --git a/command/plan.go b/command/plan.go index 44790f72bf66..7808c3879cf0 100644 --- a/command/plan.go +++ b/command/plan.go @@ -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 diff --git a/command/refresh.go b/command/refresh.go index 256bf70dd590..39b27530ac39 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -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 From 7e60a20494f0f97a94b11d36dbd9ebecaad54fc6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 14 Jul 2014 12:10:26 -0700 Subject: [PATCH 4/4] digraph: fix docs --- digraph/graphviz.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/digraph/graphviz.go b/digraph/graphviz.go index cf0c4912f7c5..db6952ebbe7f 100644 --- a/digraph/graphviz.go +++ b/digraph/graphviz.go @@ -5,7 +5,7 @@ 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 WriteDot(w io.Writer, nodes []Node) error { w.Write([]byte("digraph {\n"))