Merge pull request #20 from hashicorp/f-graph-improvements
Improved Graph Output
This commit is contained in:
commit
ca7148c904
|
@ -64,7 +64,7 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the context based on the arguments given
|
// Build the context based on the arguments given
|
||||||
ctx, err := c.Context(configPath, planStatePath)
|
ctx, err := c.Context(configPath, planStatePath, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(err.Error())
|
c.Ui.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/digraph"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GraphCommand is a Command implementation that takes a Terraform
|
// GraphCommand is a Command implementation that takes a Terraform
|
||||||
|
@ -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 {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err))
|
||||||
return 1
|
return 1
|
||||||
|
@ -53,14 +52,7 @@ func (c *GraphCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
c.Ui.Output(terraform.GraphDot(g))
|
||||||
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())
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ func (m *Meta) Colorize() *colorstring.Colorize {
|
||||||
|
|
||||||
// Context returns a Terraform Context taking into account the context
|
// Context returns a Terraform Context taking into account the context
|
||||||
// options used to initialize this meta configuration.
|
// 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()
|
opts := m.contextOpts()
|
||||||
|
|
||||||
// First try to just read the plan directly from the path given.
|
// First try to just read the plan directly from the path given.
|
||||||
|
@ -84,9 +84,11 @@ func (m *Meta) Context(path, statePath string) (*terraform.Context, error) {
|
||||||
opts.State = state
|
opts.State = state
|
||||||
ctx := terraform.NewContext(opts)
|
ctx := terraform.NewContext(opts)
|
||||||
|
|
||||||
|
if doPlan {
|
||||||
if _, err := ctx.Plan(nil); err != nil {
|
if _, err := ctx.Plan(nil); err != nil {
|
||||||
return nil, fmt.Errorf("Error running plan: %s", err)
|
return nil, fmt.Errorf("Error running plan: %s", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ctx, nil
|
return ctx, nil
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
if err != nil {
|
||||||
c.Ui.Error(err.Error())
|
c.Ui.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
|
|
|
@ -78,7 +78,7 @@ func (c *RefreshCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the context based on the arguments given
|
// Build the context based on the arguments given
|
||||||
ctx, err := c.Context(configPath, statePath)
|
ctx, err := c.Context(configPath, statePath, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(err.Error())
|
c.Ui.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
|
|
|
@ -5,13 +5,17 @@ import (
|
||||||
"io"
|
"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.
|
// 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"))
|
w.Write([]byte("digraph {\n"))
|
||||||
defer w.Write([]byte("}\n"))
|
defer w.Write([]byte("}\n"))
|
||||||
|
|
||||||
for _, n := range nodes {
|
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() {
|
for _, edge := range n.Edges() {
|
||||||
target := edge.Tail()
|
target := edge.Tail()
|
||||||
line := fmt.Sprintf("\t\"%s\" -> \"%s\" [label=\"%s\"];\n",
|
line := fmt.Sprintf("\t\"%s\" -> \"%s\" [label=\"%s\"];\n",
|
||||||
|
@ -19,4 +23,6 @@ func GenerateDot(nodes []Node, w io.Writer) {
|
||||||
w.Write([]byte(line))
|
w.Write([]byte(line))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_GenerateDot(t *testing.T) {
|
func TestWriteDot(t *testing.T) {
|
||||||
nodes := ParseBasic(`a -> b ; foo
|
nodes := ParseBasic(`a -> b ; foo
|
||||||
a -> c
|
a -> c
|
||||||
b -> d
|
b -> d
|
||||||
|
@ -18,40 +18,27 @@ b -> e
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
GenerateDot(nlist, buf)
|
if err := WriteDot(buf, nlist); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
out := string(buf.Bytes())
|
actual := strings.TrimSpace(string(buf.Bytes()))
|
||||||
if !strings.HasPrefix(out, "digraph {\n") {
|
expected := strings.TrimSpace(writeDotStr)
|
||||||
t.Fatalf("bad: %v", out)
|
if actual != expected {
|
||||||
}
|
t.Fatalf("bad: %s", actual)
|
||||||
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 !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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const writeDotStr = `
|
||||||
|
digraph {
|
||||||
|
"a";
|
||||||
|
"a" -> "b" [label="foo"];
|
||||||
|
"a" -> "c" [label="Edge"];
|
||||||
|
"b";
|
||||||
|
"b" -> "d" [label="Edge"];
|
||||||
|
"b" -> "e" [label="Edge"];
|
||||||
|
"c";
|
||||||
|
"d";
|
||||||
|
"e";
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
Loading…
Reference in New Issue