diff --git a/config/config.go b/config/config.go index d9b725821..a787d574d 100644 --- a/config/config.go +++ b/config/config.go @@ -3,14 +3,17 @@ package config import ( + "fmt" "strings" + + "github.com/hashicorp/terraform/depgraph" ) // Config is the configuration that comes from loading a collection // of Terraform templates. type Config struct { Variables map[string]Variable - Resources []Resource + Resources []*Resource } // A resource represents a single Terraform resource in the configuration. @@ -57,6 +60,70 @@ type UserVariable struct { key string } +// A unique identifier for this resource. +func (r *Resource) Id() string { + return fmt.Sprintf("%s.%s", r.Type, r.Name) +} + +// ResourceGraph returns a dependency graph of the resources from this +// Terraform configuration. +func (c *Config) ResourceGraph() *depgraph.Graph { + resource2Noun := func(r *Resource) *depgraph.Noun { + return &depgraph.Noun{ + Name: r.Id(), + Meta: r, + } + } + + nouns := make(map[string]*depgraph.Noun) + for _, r := range c.Resources { + noun := resource2Noun(r) + nouns[noun.Name] = noun + } + + for _, noun := range nouns { + r := noun.Meta.(*Resource) + for _, v := range r.Variables { + // Only resource variables impose dependencies + rv, ok := v.(*ResourceVariable) + if !ok { + continue + } + + // Build the dependency + dep := &depgraph.Dependency{ + Name: rv.ResourceId(), + Source: noun, + Target: nouns[rv.ResourceId()], + } + + noun.Deps = append(noun.Deps, dep) + } + } + + // Create the list of nouns that the depgraph.Graph struct expects + nounsList := make([]*depgraph.Noun, 0, len(nouns)) + for _, n := range nouns { + nounsList = append(nounsList, n) + } + + // Create a root that just depends on everything else finishing. + root := &depgraph.Noun{Name: "root"} + for _, n := range nounsList { + root.Deps = append(root.Deps, &depgraph.Dependency{ + Name: n.Name, + Source: root, + Target: n, + }) + } + nounsList = append(nounsList, root) + + return &depgraph.Graph{ + Name: "resources", + Nouns: nounsList, + } +} + func NewResourceVariable(key string) (*ResourceVariable, error) { parts := strings.SplitN(key, ".", 3) return &ResourceVariable{ @@ -67,6 +134,10 @@ func NewResourceVariable(key string) (*ResourceVariable, error) { }, nil } +func (v *ResourceVariable) ResourceId() string { + return fmt.Sprintf("%s.%s", v.Type, v.Name) +} + func (v *ResourceVariable) FullKey() string { return v.key } diff --git a/config/config_test.go b/config/config_test.go index e0bef17ca..bc945bee1 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,12 +1,33 @@ package config import ( + "path/filepath" + "strings" "testing" ) // This is the directory where our test fixtures are. const fixtureDir = "./test-fixtures" +func TestConfigResourceGraph(t *testing.T) { + c, err := Load(filepath.Join(fixtureDir, "resource_graph.tf")) + if err != nil { + t.Fatalf("err: %s", err) + } + + graph := c.ResourceGraph() + if err := graph.Validate(); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(graph.String()) + expected := resourceGraphValue + + if actual != strings.TrimSpace(expected) { + t.Fatalf("bad:\n%s", actual) + } +} + func TestNewResourceVariable(t *testing.T) { v, err := NewResourceVariable("foo.bar.baz") if err != nil { @@ -41,3 +62,15 @@ func TestNewUserVariable(t *testing.T) { t.Fatalf("bad: %#v", v) } } + +const resourceGraphValue = ` +root: root + root -> aws_security_group.firewall + root -> aws_instance.web +aws_security_group.firewall +aws_instance.web + aws_instance.web -> aws_security_group.firewall +root + root -> aws_security_group.firewall + root -> aws_instance.web +` diff --git a/config/config_tree.go b/config/config_tree.go index 53bb2bf68..569ceb125 100644 --- a/config/config_tree.go +++ b/config/config_tree.go @@ -68,7 +68,7 @@ func mergeConfig(c1, c2 *Config) (*Config, error) { // Merge resources: If they collide, we just take the latest one // for now. In the future, we might provide smarter merge functionality. - resources := make(map[string]Resource) + resources := make(map[string]*Resource) for _, r := range c1.Resources { id := fmt.Sprintf("%s[%s]", r.Type, r.Name) resources[id] = r @@ -78,7 +78,7 @@ func mergeConfig(c1, c2 *Config) (*Config, error) { resources[id] = r } - c.Resources = make([]Resource, 0, len(resources)) + c.Resources = make([]*Resource, 0, len(resources)) for _, r := range resources { c.Resources = append(c.Resources, r) } diff --git a/config/loader_libucl.go b/config/loader_libucl.go index 749f93dd6..dcd5be336 100644 --- a/config/loader_libucl.go +++ b/config/loader_libucl.go @@ -120,7 +120,7 @@ func loadFileLibucl(root string) (configurable, []string, error) { // The resulting resources may not be unique, but each resource // represents exactly one resource definition in the libucl configuration. // We leave it up to another pass to merge them together. -func loadResourcesLibucl(o *libucl.Object) ([]Resource, error) { +func loadResourcesLibucl(o *libucl.Object) ([]*Resource, error) { var allTypes []*libucl.Object // Libucl object iteration is really nasty. Below is likely to make @@ -154,7 +154,7 @@ func loadResourcesLibucl(o *libucl.Object) ([]Resource, error) { iter.Close() // Where all the results will go - var result []Resource + var result []*Resource // Now go over all the types and their children in order to get // all of the actual resources. @@ -186,7 +186,7 @@ func loadResourcesLibucl(o *libucl.Object) ([]Resource, error) { err) } - result = append(result, Resource{ + result = append(result, &Resource{ Name: r.Key(), Type: t.Key(), Config: config, diff --git a/config/loader_test.go b/config/loader_test.go index e26687b66..5a7886e5d 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -58,7 +58,7 @@ func TestLoadBasic_import(t *testing.T) { // This helper turns a resources field into a deterministic // string value for comparison in tests. -func resourcesStr(rs []Resource) string { +func resourcesStr(rs []*Resource) string { result := "" for _, r := range rs { result += fmt.Sprintf( diff --git a/config/test-fixtures/resource_graph.tf b/config/test-fixtures/resource_graph.tf new file mode 100644 index 000000000..9eebc208a --- /dev/null +++ b/config/test-fixtures/resource_graph.tf @@ -0,0 +1,15 @@ +variable "foo" { + default = "bar"; + description = "bar"; +} + +resource "aws_security_group" "firewall" { +} + +resource aws_instance "web" { + ami = "${var.foo}" + security_groups = [ + "foo", + "${aws_security_group.firewall.foo}" + ] +} diff --git a/depgraph/graph.go b/depgraph/graph.go index 81c1abcee..ab69aa655 100644 --- a/depgraph/graph.go +++ b/depgraph/graph.go @@ -8,7 +8,6 @@ package depgraph import ( "bytes" "fmt" - "strings" "github.com/hashicorp/terraform/digraph" ) @@ -111,33 +110,21 @@ func (g *Graph) CheckConstraints() error { func (g *Graph) String() string { var buf bytes.Buffer - cb := func(n *Noun, depth int) { + buf.WriteString(fmt.Sprintf("root: %s\n", g.Root.Name)) + for _, dep := range g.Root.Deps { buf.WriteString(fmt.Sprintf( - "%s%s\n", - strings.Repeat(" ", depth), - n.Name)) - } + " %s -> %s\n", + dep.Source, + dep.Target)) + } - type listItem struct { - n *Noun - d int - } - nodes := []listItem{{g.Root, 0}} - for len(nodes) > 0 { - // Pop current node - n := len(nodes) - current := nodes[n-1] - nodes = nodes[:n-1] - - // Visit - cb(current.n, current.d) - - // Traverse - for _, dep := range current.n.Deps { - nodes = append(nodes, listItem{ - n: dep.Target, - d: current.d + 1, - }) + for _, n := range g.Nouns { + buf.WriteString(fmt.Sprintf("%s\n", n.Name)) + for _, dep := range n.Deps { + buf.WriteString(fmt.Sprintf( + " %s -> %s\n", + dep.Source, + dep.Target)) } } diff --git a/depgraph/graph_test.go b/depgraph/graph_test.go index d80f37646..e9d1dee9d 100644 --- a/depgraph/graph_test.go +++ b/depgraph/graph_test.go @@ -69,13 +69,20 @@ c -> e`) actual := g.String() expected := ` +root: a + a -> b + a -> c a - c - e - d - b - e - d + a -> b + a -> c +b + b -> d + b -> e +c + c -> d + c -> e +d +e ` actual = strings.TrimSpace(actual)