config: can generate depgraph
This commit is contained in:
parent
f7a50503b7
commit
dac18c823a
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
`
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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}"
|
||||
]
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue