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
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
}

View File

@ -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
`

View File

@ -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)
}

View File

@ -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,

View File

@ -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(

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 (
"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))
}
}

View File

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