config: can generate depgraph
This commit is contained in:
parent
f7a50503b7
commit
dac18c823a
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
`
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 (
|
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
e
|
b -> d
|
||||||
|
b -> e
|
||||||
|
c
|
||||||
|
c -> d
|
||||||
|
c -> e
|
||||||
d
|
d
|
||||||
|
e
|
||||||
`
|
`
|
||||||
|
|
||||||
actual = strings.TrimSpace(actual)
|
actual = strings.TrimSpace(actual)
|
||||||
|
|
Loading…
Reference in New Issue