diff --git a/depgraph2/graph.go b/depgraph2/graph.go new file mode 100644 index 0000000000..059f8ea972 --- /dev/null +++ b/depgraph2/graph.go @@ -0,0 +1,70 @@ +package depgraph + +import ( + "bytes" + "fmt" + "sort" +) + +// Graph is used to represent a dependency graph. +type Graph struct { + Nodes []Node +} + +// Node is an element of the graph that has other dependencies. +type Node interface { + Deps() []Node +} + +// NamedNode is an optional interface implementation of a Node that +// can have a name. If this is implemented, this will be used for various +// output. +type NamedNode interface { + Node + Name() string +} + +func (g *Graph) String() string { + var buf bytes.Buffer + + // Build the list of node names and a mapping so that we can more + // easily alphabetize the output to remain deterministic. + names := make([]string, 0, len(g.Nodes)) + mapping := make(map[string]Node, len(g.Nodes)) + for _, n := range g.Nodes { + name := nodeName(n) + names = append(names, name) + mapping[name] = n + } + sort.Strings(names) + + // Write each node in order... + for _, name := range names { + n := mapping[name] + buf.WriteString(fmt.Sprintf("%s\n", name)) + + // Alphabetize dependencies + depsRaw := n.Deps() + deps := make([]string, 0, len(depsRaw)) + for _, d := range depsRaw { + deps = append(deps, nodeName(d)) + } + sort.Strings(deps) + + // Write dependencies + for _, d := range deps { + buf.WriteString(fmt.Sprintf(" %s\n", d)) + } + } + + return buf.String() +} + +func nodeName(n Node) string { + switch v := n.(type) { + case NamedNode: + return v.Name() + default: + return fmt.Sprintf("%s", v) + } +} diff --git a/terraform/graph2.go b/terraform/graph2.go new file mode 100644 index 0000000000..1e2eadd4e0 --- /dev/null +++ b/terraform/graph2.go @@ -0,0 +1,146 @@ +package terraform + +import ( + "errors" + "fmt" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/depgraph2" +) + +// Graph takes a module tree and builds a logical graph of all the nodes +// in that module. +func Graph2(mod *module.Tree) (*depgraph.Graph, error) { + // A module is required and also must be completely loaded. + if mod == nil { + return nil, errors.New("module must not be nil") + } + if !mod.Loaded() { + return nil, errors.New("module must be loaded") + } + + // Get the configuration for this module + config := mod.Config() + + // Create the node list we'll use for the graph + nodes := make([]graphNodeConfig, 0, + (len(config.Modules)+len(config.Resources))*2) + + // Write all the resources out + for _, r := range config.Resources { + nodes = append(nodes, &GraphNodeConfigResource{ + Resource: r, + }) + } + + // Write all the modules out + // TODO + + // Build the full map of the var names to the nodes. + fullMap := make(map[string]depgraph.Node) + for _, n := range nodes { + fullMap[n.VarName()] = n + } + + // Go through all the nodes and build up the actual dependency map. We + // do this by getting the variables that each node depends on and then + // building the dep map based on the fullMap which contains the mapping + // of var names to the actual node with that name. + for _, n := range nodes { + m := make(map[string]depgraph.Node) + for _, v := range n.Variables() { + id := varNameForVar(v) + m[id] = fullMap[id] + } + + n.setDepMap(m) + } + + // Build the graph and return it + g := &depgraph.Graph{Nodes: make([]depgraph.Node, 0, len(nodes))} + for _, n := range nodes { + g.Nodes = append(g.Nodes, n) + } + + return g, nil +} + +// graphNodeConfig is an interface that all graph nodes for the +// configuration graph need to implement in order to build the variable +// dependencies properly. +type graphNodeConfig interface { + depgraph.Node + + // Variables returns the full list of variables that this node + // depends on. + Variables() map[string]config.InterpolatedVariable + + // VarName returns the name that is used to identify a variable + // maps to this node. It should match the result of the + // `VarName` function. + VarName() string + + // setDepMap sets the dependency map for this node. If the node is + // nil, then it wasn't found. + setDepMap(map[string]depgraph.Node) +} + +// GraphNodeConfigResource represents a resource within the configuration +// graph. +type GraphNodeConfigResource struct { + Resource *config.Resource + DepMap map[string]depgraph.Node +} + +func (n *GraphNodeConfigResource) Deps() []depgraph.Node { + r := make([]depgraph.Node, 0, len(n.DepMap)) + for _, v := range n.DepMap { + if v != nil { + r = append(r, v) + } + } + + return r +} + +func (n *GraphNodeConfigResource) Name() string { + return n.Resource.Id() +} + +func (n *GraphNodeConfigResource) Variables() map[string]config.InterpolatedVariable { + var m map[string]config.InterpolatedVariable + if n.Resource != nil { + m = make(map[string]config.InterpolatedVariable) + for k, v := range n.Resource.RawCount.Variables { + m[k] = v + } + for k, v := range n.Resource.RawConfig.Variables { + m[k] = v + } + } + + return m +} + +func (n *GraphNodeConfigResource) VarName() string { + return n.Resource.Id() +} + +func (n *GraphNodeConfigResource) setDepMap(m map[string]depgraph.Node) { + n.DepMap = m +} + +// varNameForVar returns the VarName value for an interpolated variable. +// This value is compared to the VarName() value for the nodes within the +// graph to build the graph edges. +func varNameForVar(raw config.InterpolatedVariable) string { + switch v := raw.(type) { + case *config.ModuleVariable: + return fmt.Sprintf("module.%s", v.Name) + case *config.ResourceVariable: + return v.ResourceId() + default: + return "" + } +} diff --git a/terraform/graph2_test.go b/terraform/graph2_test.go new file mode 100644 index 0000000000..a781b53b67 --- /dev/null +++ b/terraform/graph2_test.go @@ -0,0 +1,50 @@ +package terraform + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/hashicorp/terraform/config/module" +) + +func TestGraph_nilModule(t *testing.T) { + _, err := Graph2(nil) + if err == nil { + t.Fatal("should error") + } +} + +func TestGraph_unloadedModule(t *testing.T) { + mod, err := module.NewTreeModule( + "", filepath.Join(fixtureDir, "graph-basic")) + if err != nil { + t.Fatalf("err: %s", err) + } + + if _, err := Graph2(mod); err == nil { + t.Fatal("should error") + } +} + +func TestGraph(t *testing.T) { + g, err := Graph2(testModule(t, "graph-basic")) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testGraphBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +const testGraphBasicStr = ` +aws_instance.web + aws_security_group.firewall +aws_load_balancer.weblb + aws_instance.web +aws_security_group.firewall +openstack_floating_ip.random +`