config: can generate depgraph

This commit is contained in:
Mitchell Hashimoto 2014-05-24 13:57:51 -07:00
parent f7a50503b7
commit dac18c823a
8 changed files with 152 additions and 39 deletions

View File

@ -3,14 +3,17 @@
package config package config
import ( import (
"fmt"
"strings" "strings"
"github.com/hashicorp/terraform/depgraph"
) )
// Config is the configuration that comes from loading a collection // Config is the configuration that comes from loading a collection
// of Terraform templates. // of Terraform templates.
type Config struct { type Config struct {
Variables map[string]Variable Variables map[string]Variable
Resources []Resource Resources []*Resource
} }
// A resource represents a single Terraform resource in the configuration. // A resource represents a single Terraform resource in the configuration.
@ -57,6 +60,70 @@ type UserVariable struct {
key string 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) { func NewResourceVariable(key string) (*ResourceVariable, error) {
parts := strings.SplitN(key, ".", 3) parts := strings.SplitN(key, ".", 3)
return &ResourceVariable{ return &ResourceVariable{
@ -67,6 +134,10 @@ func NewResourceVariable(key string) (*ResourceVariable, error) {
}, nil }, nil
} }
func (v *ResourceVariable) ResourceId() string {
return fmt.Sprintf("%s.%s", v.Type, v.Name)
}
func (v *ResourceVariable) FullKey() string { func (v *ResourceVariable) FullKey() string {
return v.key return v.key
} }

View File

@ -1,12 +1,33 @@
package config package config
import ( import (
"path/filepath"
"strings"
"testing" "testing"
) )
// This is the directory where our test fixtures are. // This is the directory where our test fixtures are.
const fixtureDir = "./test-fixtures" 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) { func TestNewResourceVariable(t *testing.T) {
v, err := NewResourceVariable("foo.bar.baz") v, err := NewResourceVariable("foo.bar.baz")
if err != nil { if err != nil {
@ -41,3 +62,15 @@ func TestNewUserVariable(t *testing.T) {
t.Fatalf("bad: %#v", v) 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
`

View File

@ -68,7 +68,7 @@ func mergeConfig(c1, c2 *Config) (*Config, error) {
// Merge resources: If they collide, we just take the latest one // Merge resources: If they collide, we just take the latest one
// for now. In the future, we might provide smarter merge functionality. // 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 { for _, r := range c1.Resources {
id := fmt.Sprintf("%s[%s]", r.Type, r.Name) id := fmt.Sprintf("%s[%s]", r.Type, r.Name)
resources[id] = r resources[id] = r
@ -78,7 +78,7 @@ func mergeConfig(c1, c2 *Config) (*Config, error) {
resources[id] = r resources[id] = r
} }
c.Resources = make([]Resource, 0, len(resources)) c.Resources = make([]*Resource, 0, len(resources))
for _, r := range resources { for _, r := range resources {
c.Resources = append(c.Resources, r) c.Resources = append(c.Resources, r)
} }

View File

@ -120,7 +120,7 @@ func loadFileLibucl(root string) (configurable, []string, error) {
// The resulting resources may not be unique, but each resource // The resulting resources may not be unique, but each resource
// represents exactly one resource definition in the libucl configuration. // represents exactly one resource definition in the libucl configuration.
// We leave it up to another pass to merge them together. // 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 var allTypes []*libucl.Object
// Libucl object iteration is really nasty. Below is likely to make // Libucl object iteration is really nasty. Below is likely to make
@ -154,7 +154,7 @@ func loadResourcesLibucl(o *libucl.Object) ([]Resource, error) {
iter.Close() iter.Close()
// Where all the results will go // 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 // Now go over all the types and their children in order to get
// all of the actual resources. // all of the actual resources.
@ -186,7 +186,7 @@ func loadResourcesLibucl(o *libucl.Object) ([]Resource, error) {
err) err)
} }
result = append(result, Resource{ result = append(result, &Resource{
Name: r.Key(), Name: r.Key(),
Type: t.Key(), Type: t.Key(),
Config: config, Config: config,

View File

@ -58,7 +58,7 @@ func TestLoadBasic_import(t *testing.T) {
// This helper turns a resources field into a deterministic // This helper turns a resources field into a deterministic
// string value for comparison in tests. // string value for comparison in tests.
func resourcesStr(rs []Resource) string { func resourcesStr(rs []*Resource) string {
result := "" result := ""
for _, r := range rs { for _, r := range rs {
result += fmt.Sprintf( result += fmt.Sprintf(

View File

@ -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}"
]
}

View File

@ -8,7 +8,6 @@ package depgraph
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"strings"
"github.com/hashicorp/terraform/digraph" "github.com/hashicorp/terraform/digraph"
) )
@ -111,33 +110,21 @@ func (g *Graph) CheckConstraints() error {
func (g *Graph) String() string { func (g *Graph) String() string {
var buf bytes.Buffer 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( buf.WriteString(fmt.Sprintf(
"%s%s\n", " %s -> %s\n",
strings.Repeat(" ", depth), dep.Source,
n.Name)) dep.Target))
} }
type listItem struct { for _, n := range g.Nouns {
n *Noun buf.WriteString(fmt.Sprintf("%s\n", n.Name))
d int for _, dep := range n.Deps {
} buf.WriteString(fmt.Sprintf(
nodes := []listItem{{g.Root, 0}} " %s -> %s\n",
for len(nodes) > 0 { dep.Source,
// Pop current node dep.Target))
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,
})
} }
} }

View File

@ -69,13 +69,20 @@ c -> e`)
actual := g.String() actual := g.String()
expected := ` expected := `
root: a
a -> b
a -> c
a a
c a -> b
e a -> c
d b
b b -> d
e b -> e
d c
c -> d
c -> e
d
e
` `
actual = strings.TrimSpace(actual) actual = strings.TrimSpace(actual)