terraform: update graph to build subgraphs for modules

This commit is contained in:
Mitchell Hashimoto 2014-09-22 16:48:18 -07:00
parent 97da02c368
commit a6f792b3aa
10 changed files with 185 additions and 29 deletions

View File

@ -1023,6 +1023,9 @@ func (c *Context) genericWalkFn(cb genericWalkFunc) depgraph.WalkFunc {
}() }()
switch m := n.Meta.(type) { switch m := n.Meta.(type) {
case *GraphNodeModule:
// TODO
return nil
case *GraphNodeResource: case *GraphNodeResource:
// Continue, we care about this the most // Continue, we care about this the most
case *GraphNodeResourceMeta: case *GraphNodeResourceMeta:

View File

@ -2179,6 +2179,67 @@ func TestContextRefresh_hook(t *testing.T) {
*/ */
} }
func TestContextRefresh_modules(t *testing.T) {
p := testProvider("aws")
m := testModule(t, "refresh-modules")
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Tainted: []*InstanceState{
&InstanceState{
ID: "bar",
},
},
},
},
},
&ModuleState{
Path: []string{"root", "child"},
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "baz",
},
},
},
},
},
}
ctx := testContext(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
State: state,
})
p.RefreshFn = func(info *InstanceInfo, s *InstanceState) (*InstanceState, error) {
if s.ID != "baz" {
return s, nil
}
s.ID = "new"
return s, nil
}
s, err := ctx.Refresh()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(s.String())
expected := strings.TrimSpace(testContextRefreshModuleStr)
if actual != expected {
t.Fatalf("bad:\n\n%s\n\n%s", actual, expected)
}
}
func TestContextRefresh_state(t *testing.T) { func TestContextRefresh_state(t *testing.T) {
p := testProvider("aws") p := testProvider("aws")
m := testModule(t, "refresh-basic") m := testModule(t, "refresh-basic")
@ -2476,6 +2537,16 @@ root
root -> aws_instance.foo root -> aws_instance.foo
` `
const testContextRefreshModuleStr = `
aws_instance.web: (1 tainted)
ID = <not created>
Tainted ID 1 = foo
module.child:
aws_instance.web:
ID = new
`
const testContextRefreshTaintedStr = ` const testContextRefreshTaintedStr = `
aws_instance.web: (1 tainted) aws_instance.web: (1 tainted)
ID = <not created> ID = <not created>

View File

@ -24,8 +24,13 @@ type GraphOpts struct {
//Config *config.Config //Config *config.Config
// Module is the relative root of a module tree for this graph. This // Module is the relative root of a module tree for this graph. This
// is the only required item. // is the only required item. This should always be the absolute root
Module *module.Tree // of the tree. ModulePath below should be used to constrain the depth.
//
// ModulePath specifies the place in the tree where Module exists.
// This is used for State lookups.
Module *module.Tree
ModulePath []string
// Diff of changes that will be applied to the given state. This will // Diff of changes that will be applied to the given state. This will
// associate a ResourceDiff with applicable resources. Additionally, // associate a ResourceDiff with applicable resources. Additionally,
@ -59,6 +64,8 @@ const GraphRootNode = "root"
// that will be created/managed. // that will be created/managed.
type GraphNodeModule struct { type GraphNodeModule struct {
Config *config.Module Config *config.Module
Path []string
Graph *depgraph.Graph
} }
// GraphNodeResource is a node type in the graph that represents a resource // GraphNodeResource is a node type in the graph that represents a resource
@ -109,8 +116,27 @@ func Graph(opts *GraphOpts) (*depgraph.Graph, error) {
if opts.Module == nil { if opts.Module == nil {
return nil, errors.New("Module is required for Graph") return nil, errors.New("Module is required for Graph")
} }
if opts.ModulePath == nil {
opts.ModulePath = rootModulePath
}
if !opts.Module.Loaded() {
return nil, errors.New("Module must be loaded")
}
config := opts.Module.Config() // Get the correct module in the tree that we're looking for.
currentModule := opts.Module
for _, n := range opts.ModulePath[1:] {
children := currentModule.Children()
currentModule = children[n]
}
config := currentModule.Config()
// Get the state of the module that we're working with.
var mod *ModuleState
if opts.State != nil {
mod = opts.State.ModuleByPath(opts.ModulePath)
}
log.Printf("[DEBUG] Creating graph...") log.Printf("[DEBUG] Creating graph...")
@ -119,7 +145,7 @@ func Graph(opts *GraphOpts) (*depgraph.Graph, error) {
// First, build the initial resource graph. This only has the resources // First, build the initial resource graph. This only has the resources
// and no dependencies. This only adds resources that are in the config // and no dependencies. This only adds resources that are in the config
// and not "orphans" (that are in the state, but not in the config). // and not "orphans" (that are in the state, but not in the config).
graphAddConfigResources(g, config, opts.State) graphAddConfigResources(g, config, mod)
// Add the modules that are in the configuration. // Add the modules that are in the configuration.
graphAddConfigModules(g, config, opts) graphAddConfigModules(g, config, opts)
@ -127,12 +153,12 @@ func Graph(opts *GraphOpts) (*depgraph.Graph, error) {
// Add explicit dependsOn dependencies to the graph // Add explicit dependsOn dependencies to the graph
graphAddExplicitDeps(g) graphAddExplicitDeps(g)
if opts.State != nil { if mod != nil {
// Next, add the state orphans if we have any // Next, add the state orphans if we have any
graphAddOrphans(g, config, opts.State) graphAddOrphans(g, config, mod)
// Add tainted resources if we have any. // Add tainted resources if we have any.
graphAddTainted(g, opts.State) graphAddTainted(g, mod)
} }
// Map the provider configurations to all of the resources // Map the provider configurations to all of the resources
@ -238,20 +264,39 @@ func graphInitState(s *State, g *depgraph.Graph) {
// graphAddConfigModules adds the modules from a configuration structure // graphAddConfigModules adds the modules from a configuration structure
// into the graph, expanding each to their own sub-graph. // into the graph, expanding each to their own sub-graph.
func graphAddConfigModules(g *depgraph.Graph, c *config.Config, opts *GraphOpts) { func graphAddConfigModules(
g *depgraph.Graph,
c *config.Config,
opts *GraphOpts) error {
// Just short-circuit the whole thing if we don't have modules // Just short-circuit the whole thing if we don't have modules
if len(c.Modules) == 0 { if len(c.Modules) == 0 {
return return nil
} }
// Build the list of nouns to add to the graph // Build the list of nouns to add to the graph
nounsList := make([]*depgraph.Noun, 0, len(c.Modules)) nounsList := make([]*depgraph.Noun, 0, len(c.Modules))
for _, m := range c.Modules { for _, m := range c.Modules {
name := fmt.Sprintf("module.%s", m.Name) name := fmt.Sprintf("module.%s", m.Name)
path := make([]string, len(opts.ModulePath)+1)
copy(path, opts.ModulePath)
path[len(opts.ModulePath)] = m.Name
// Build the opts we'll use to make the next graph
subOpts := *opts
subOpts.ModulePath = path
subGraph, err := Graph(&subOpts)
if err != nil {
return fmt.Errorf(
"Error building module graph '%s': %s",
m.Name, err)
}
n := &depgraph.Noun{ n := &depgraph.Noun{
Name: name, Name: name,
Meta: &GraphNodeModule{ Meta: &GraphNodeModule{
Config: m, Config: m,
Path: path,
Graph: subGraph,
}, },
} }
@ -259,14 +304,12 @@ func graphAddConfigModules(g *depgraph.Graph, c *config.Config, opts *GraphOpts)
} }
g.Nouns = append(g.Nouns, nounsList...) g.Nouns = append(g.Nouns, nounsList...)
return nil
} }
// configGraph turns a configuration structure into a dependency graph. // configGraph turns a configuration structure into a dependency graph.
func graphAddConfigResources( func graphAddConfigResources(
g *depgraph.Graph, c *config.Config, s *State) { g *depgraph.Graph, c *config.Config, mod *ModuleState) {
// TODO: Handle non-root modules
mod := s.ModuleByPath(rootModulePath)
// This tracks all the resource nouns // This tracks all the resource nouns
nouns := make(map[string]*depgraph.Noun) nouns := make(map[string]*depgraph.Noun)
for _, r := range c.Resources { for _, r := range c.Resources {
@ -283,7 +326,7 @@ func graphAddConfigResources(
} }
var state *ResourceState var state *ResourceState
if s != nil && mod != nil { if mod != nil {
// Lookup the resource state // Lookup the resource state
state = mod.Resources[name] state = mod.Resources[name]
if state == nil { if state == nil {
@ -633,12 +676,7 @@ func graphAddMissingResourceProviders(
} }
// graphAddOrphans adds the orphans to the graph. // graphAddOrphans adds the orphans to the graph.
func graphAddOrphans(g *depgraph.Graph, c *config.Config, s *State) { func graphAddOrphans(g *depgraph.Graph, c *config.Config, mod *ModuleState) {
// TODO: Handle other modules
mod := s.ModuleByPath(rootModulePath)
if mod == nil {
return
}
var nlist []*depgraph.Noun var nlist []*depgraph.Noun
for _, k := range mod.Orphans(c) { for _, k := range mod.Orphans(c) {
rs := mod.Resources[k] rs := mod.Resources[k]
@ -826,13 +864,7 @@ func graphAddVariableDeps(g *depgraph.Graph) {
} }
// graphAddTainted adds the tainted instances to the graph. // graphAddTainted adds the tainted instances to the graph.
func graphAddTainted(g *depgraph.Graph, s *State) { func graphAddTainted(g *depgraph.Graph, mod *ModuleState) {
// TODO: Handle other modules
mod := s.ModuleByPath(rootModulePath)
if mod == nil {
return
}
var nlist []*depgraph.Noun var nlist []*depgraph.Noun
for k, rs := range mod.Resources { for k, rs := range mod.Resources {
// If we have no tainted resources, continue on // If we have no tainted resources, continue on

View File

@ -94,6 +94,22 @@ func TestGraph_modules(t *testing.T) {
if actual != expected { if actual != expected {
t.Fatalf("bad:\n\n%s", actual) t.Fatalf("bad:\n\n%s", actual)
} }
n := g.Noun("module.consul")
if n == nil {
t.Fatal("can't find noun")
}
mn := n.Meta.(*GraphNodeModule)
if !reflect.DeepEqual(mn.Path, []string{"root", "consul"}) {
t.Fatalf("bad: %#v", mn.Path)
}
actual = strings.TrimSpace(mn.Graph.String())
expected = strings.TrimSpace(testTerraformGraphModulesConsulStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
} }
func TestGraph_state(t *testing.T) { func TestGraph_state(t *testing.T) {
@ -799,6 +815,13 @@ root
root -> module.consul root -> module.consul
` `
const testTerraformGraphModulesConsulStr = `
root: root
aws_instance.server
root
root -> aws_instance.server
`
const testTerraformGraphStateStr = ` const testTerraformGraphStateStr = `
root: root root: root
aws_instance.old aws_instance.old

View File

@ -114,11 +114,11 @@ func (s *State) String() string {
// If we're the root module, we just write the output directly. // If we're the root module, we just write the output directly.
if reflect.DeepEqual(m.Path, rootModulePath) { if reflect.DeepEqual(m.Path, rootModulePath) {
buf.WriteString(mStr) buf.WriteString(mStr+"\n")
continue continue
} }
buf.WriteString(fmt.Sprintf("%s:\n", strings.Join(m.Path, "."))) buf.WriteString(fmt.Sprintf("module.%s:\n", strings.Join(m.Path[1:], ".")))
s := bufio.NewScanner(strings.NewReader(mStr)) s := bufio.NewScanner(strings.NewReader(mStr))
for s.Scan() { for s.Scan() {

View File

@ -5,6 +5,8 @@ import (
"crypto/sha1" "crypto/sha1"
"encoding/gob" "encoding/gob"
"encoding/hex" "encoding/hex"
"io/ioutil"
"os"
"path/filepath" "path/filepath"
"sync" "sync"
"testing" "testing"
@ -31,6 +33,18 @@ func checksumStruct(t *testing.T, i interface{}) string {
return hex.EncodeToString(sum[:]) return hex.EncodeToString(sum[:])
} }
func tempDir(t *testing.T) string {
dir, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.RemoveAll(dir); err != nil {
t.Fatalf("err: %s", err)
}
return dir
}
func testConfig(t *testing.T, name string) *config.Config { func testConfig(t *testing.T, name string) *config.Config {
c, err := config.Load(filepath.Join(fixtureDir, name, "main.tf")) c, err := config.Load(filepath.Join(fixtureDir, name, "main.tf"))
if err != nil { if err != nil {
@ -46,6 +60,11 @@ func testModule(t *testing.T, name string) *module.Tree {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
s := &module.FolderStorage{StorageDir: tempDir(t)}
if err := mod.Load(s, module.GetModeGet); err != nil {
t.Fatalf("err: %s", err)
}
return mod return mod
} }

View File

@ -0,0 +1 @@
resource "aws_instance" "server" {}

View File

@ -1,5 +1,6 @@
module "consul" { module "consul" {
foo = "${aws_security_group.firewall.foo}" foo = "${aws_security_group.firewall.foo}"
source = "./consul"
} }
provider "aws" {} provider "aws" {}

View File

@ -0,0 +1 @@
resource "aws_instance" "web" {}

View File

@ -0,0 +1,5 @@
module "child" {
source = "./child"
}
resource "aws_instance" "web" {}