Merge pull request #598 from hashicorp/f-mod-deps

Fix dependency handling of modules
This commit is contained in:
Armon Dadgar 2014-11-24 18:53:22 -08:00
commit f26b8df92f
9 changed files with 407 additions and 99 deletions

View File

@ -929,6 +929,13 @@ func (c *walkContext) planDestroyWalkFn() depgraph.WalkFunc {
walkFn = func(n *depgraph.Noun) error { walkFn = func(n *depgraph.Noun) error {
switch m := n.Meta.(type) { switch m := n.Meta.(type) {
case *GraphNodeModule: case *GraphNodeModule:
// Set the destroy bool on the module
md := result.Diff.ModuleByPath(m.Path)
if md == nil {
md = result.Diff.AddModule(m.Path)
}
md.Destroy = true
// Build another walkContext for this module and walk it. // Build another walkContext for this module and walk it.
wc := c.Context.walkContext(c.Operation, m.Path) wc := c.Context.walkContext(c.Operation, m.Path)

View File

@ -114,6 +114,7 @@ func (d *Diff) init() {
type ModuleDiff struct { type ModuleDiff struct {
Path []string Path []string
Resources map[string]*InstanceDiff Resources map[string]*InstanceDiff
Destroy bool // Set only by the destroy plan
} }
func (d *ModuleDiff) init() { func (d *ModuleDiff) init() {
@ -192,6 +193,10 @@ func (d *ModuleDiff) IsRoot() bool {
func (d *ModuleDiff) String() string { func (d *ModuleDiff) String() string {
var buf bytes.Buffer var buf bytes.Buffer
if d.Destroy {
buf.WriteString("DESTROY MODULE\n")
}
names := make([]string, 0, len(d.Resources)) names := make([]string, 0, len(d.Resources))
for name, _ := range d.Resources { for name, _ := range d.Resources {
names = append(names, name) names = append(names, name)

View File

@ -80,6 +80,8 @@ type GraphNodeModule struct {
Config *config.Module Config *config.Module
Path []string Path []string
Graph *depgraph.Graph Graph *depgraph.Graph
State *ModuleState
Flags ResourceFlag
} }
// GraphNodeResource is a node type in the graph that represents a resource // GraphNodeResource is a node type in the graph that represents a resource
@ -246,6 +248,9 @@ func Graph(opts *GraphOpts) (*depgraph.Graph, error) {
// Add the orphan dependencies // Add the orphan dependencies
graphAddOrphanDeps(g, modState) graphAddOrphanDeps(g, modState)
// Add the orphan module dependencies
graphAddOrphanModuleDeps(g, modState)
// Add the provider dependencies // Add the provider dependencies
graphAddResourceProviderDeps(g) graphAddResourceProviderDeps(g)
@ -269,7 +274,7 @@ func Graph(opts *GraphOpts) (*depgraph.Graph, error) {
// If we have a diff, then make sure to add that in // If we have a diff, then make sure to add that in
if modDiff != nil { if modDiff != nil {
if err := graphAddDiff(g, modDiff); err != nil { if err := graphAddDiff(g, opts.Diff, modDiff); err != nil {
return nil, err return nil, err
} }
} }
@ -298,45 +303,70 @@ func Graph(opts *GraphOpts) (*depgraph.Graph, error) {
// allows orphaned resources to be destroyed in the proper order. // allows orphaned resources to be destroyed in the proper order.
func graphEncodeDependencies(g *depgraph.Graph) { func graphEncodeDependencies(g *depgraph.Graph) {
for _, n := range g.Nouns { for _, n := range g.Nouns {
// Ignore any non-resource nodes switch rn := n.Meta.(type) {
rn, ok := n.Meta.(*GraphNodeResource) case *GraphNodeResource:
if !ok { // If we are using create-before-destroy, there
continue // are some special depedencies injected on the
} // deposed node that would cause a circular depedency
r := rn.Resource // chain if persisted. We must only handle the new node,
// node the deposed node.
r := rn.Resource
if r.Flags&FlagDeposed != 0 {
continue
}
// If we are using create-before-destroy, there // Update the dependencies
// are some special depedencies injected on the var inject []string
// deposed node that would cause a circular depedency for _, dep := range n.Deps {
// chain if persisted. We must only handle the new node, switch target := dep.Target.Meta.(type) {
// node the deposed node. case *GraphNodeModule:
if r.Flags&FlagDeposed != 0 { inject = append(inject, dep.Target.Name)
continue
}
// Update the dependencies case *GraphNodeResource:
var inject []string if target.Resource.Id == r.Id {
for _, dep := range n.Deps { continue
switch target := dep.Target.Meta.(type) { }
case *GraphNodeModule: inject = append(inject, target.Resource.Id)
inject = append(inject, dep.Target.Name)
case *GraphNodeResource: case *GraphNodeResourceProvider:
if target.Resource.Id == r.Id { // Do nothing
continue
default:
panic(fmt.Sprintf("Unknown graph node: %#v", dep.Target))
} }
inject = append(inject, target.Resource.Id) }
case *GraphNodeResourceProvider: // Update the dependencies
// Do nothing r.Dependencies = inject
default: case *GraphNodeModule:
panic(fmt.Sprintf("Unknown graph node: %#v", dep.Target)) // Update the dependencies
var inject []string
for _, dep := range n.Deps {
switch target := dep.Target.Meta.(type) {
case *GraphNodeModule:
if dep.Target.Name == n.Name {
continue
}
inject = append(inject, dep.Target.Name)
case *GraphNodeResource:
inject = append(inject, target.Resource.Id)
case *GraphNodeResourceProvider:
// Do nothing
default:
panic(fmt.Sprintf("Unknown graph node: %#v", dep.Target))
}
}
// Update the dependencies
if rn.State != nil {
rn.State.Dependencies = inject
} }
} }
// Update the dependencies
r.Dependencies = inject
} }
} }
@ -357,6 +387,14 @@ func graphAddConfigModules(
if n, err := graphModuleNoun(m.Name, m, g, opts); err != nil { if n, err := graphModuleNoun(m.Name, m, g, opts); err != nil {
return err return err
} else { } else {
// Attach the module state if any
if opts.State != nil {
module := n.Meta.(*GraphNodeModule)
module.State = opts.State.ModuleByPath(module.Path)
if module.State == nil {
module.State = opts.State.AddModule(module.Path)
}
}
nounsList = append(nounsList, n) nounsList = append(nounsList, n)
} }
} }
@ -506,10 +544,22 @@ func graphAddConfigResources(
// destroying the VPC's subnets first, whereas creating a VPC requires // destroying the VPC's subnets first, whereas creating a VPC requires
// doing it before the subnets are created. This function handles inserting // doing it before the subnets are created. This function handles inserting
// these nodes for you. // these nodes for you.
func graphAddDiff(g *depgraph.Graph, d *ModuleDiff) error { func graphAddDiff(g *depgraph.Graph, gDiff *Diff, d *ModuleDiff) error {
var nlist []*depgraph.Noun var nlist []*depgraph.Noun
var modules []*depgraph.Noun
injected := make(map[*depgraph.Dependency]struct{}) injected := make(map[*depgraph.Dependency]struct{})
for _, n := range g.Nouns { for _, n := range g.Nouns {
// A module is being destroyed if all it's resources are being
// destroyed (via a destroy plan) or if it is orphaned. Only in
// those cases do we need to handle depedency inversion.
if mod, ok := n.Meta.(*GraphNodeModule); ok {
md := gDiff.ModuleByPath(mod.Path)
if mod.Flags&FlagOrphan != 0 || (md != nil && md.Destroy) {
modules = append(modules, n)
}
continue
}
rn, ok := n.Meta.(*GraphNodeResource) rn, ok := n.Meta.(*GraphNodeResource)
if !ok { if !ok {
continue continue
@ -655,78 +705,81 @@ func graphAddDiff(g *depgraph.Graph, d *ModuleDiff) error {
rn.Resource.Diff = rd rn.Resource.Diff = rd
} }
// Go through each noun and make sure we calculate all the dependencies // Go through each resource and module and make sure we
// properly. // calculate all the dependencies properly.
for _, n := range nlist { invertDeps := [][]*depgraph.Noun{nlist, modules}
deps := n.Deps for _, list := range invertDeps {
num := len(deps) for _, n := range list {
for i := 0; i < num; i++ { deps := n.Deps
dep := deps[i] num := len(deps)
for i := 0; i < num; i++ {
dep := deps[i]
// Check if this dependency was just injected, otherwise // Check if this dependency was just injected, otherwise
// we will incorrectly flip the depedency twice. // we will incorrectly flip the depedency twice.
if _, ok := injected[dep]; ok { if _, ok := injected[dep]; ok {
continue continue
} }
switch target := dep.Target.Meta.(type) { switch target := dep.Target.Meta.(type) {
case *GraphNodeResource: case *GraphNodeResource:
// If the other node is also being deleted, // If the other node is also being deleted,
// we must be deleted first. E.g. if A -> B, // we must be deleted first. E.g. if A -> B,
// then when we create, B is created first then A. // then when we create, B is created first then A.
// On teardown, A is destroyed first, then B. // On teardown, A is destroyed first, then B.
// Thus we must flip our depedency and instead inject // Thus we must flip our depedency and instead inject
// it on B. // it on B.
for _, n2 := range nlist { for _, n2 := range nlist {
rn2 := n2.Meta.(*GraphNodeResource) rn2 := n2.Meta.(*GraphNodeResource)
if target.Resource.Id == rn2.Resource.Id { if target.Resource.Id == rn2.Resource.Id {
newDep := &depgraph.Dependency{ newDep := &depgraph.Dependency{
Name: n.Name, Name: n.Name,
Source: n2, Source: n2,
Target: n, Target: n,
}
injected[newDep] = struct{}{}
n2.Deps = append(n2.Deps, newDep)
break
} }
injected[newDep] = struct{}{}
n2.Deps = append(n2.Deps, newDep)
break
} }
// Drop the dependency. We may have created
// an inverse depedency if the dependent resource
// is also being deleted, but this dependence is
// no longer required.
deps[i], deps[num-1] = deps[num-1], nil
num--
i--
case *GraphNodeModule:
// We invert any module dependencies so we're destroyed
// first, before any modules are applied.
newDep := &depgraph.Dependency{
Name: n.Name,
Source: dep.Target,
Target: n,
}
dep.Target.Deps = append(dep.Target.Deps, newDep)
// Drop the dependency. We may have created
// an inverse depedency if the dependent resource
// is also being deleted, but this dependence is
// no longer required.
deps[i], deps[num-1] = deps[num-1], nil
num--
i--
case *GraphNodeResourceProvider:
// Keep these around, but fix up the source to be ourselves
// rather than the old node.
newDep := *dep
newDep.Source = n
deps[i] = &newDep
default:
panic(fmt.Errorf("Unhandled depedency type: %#v", dep.Target.Meta))
} }
// Drop the dependency. We may have created
// an inverse depedency if the dependent resource
// is also being deleted, but this dependence is
// no longer required.
deps[i], deps[num-1] = deps[num-1], nil
num--
i--
case *GraphNodeModule:
// We invert any module dependencies so we're destroyed
// first, before any modules are applied.
newDep := &depgraph.Dependency{
Name: n.Name,
Source: dep.Target,
Target: n,
}
dep.Target.Deps = append(dep.Target.Deps, newDep)
// Drop the dependency. We may have created
// an inverse depedency if the dependent resource
// is also being deleted, but this dependence is
// no longer required.
deps[i], deps[num-1] = deps[num-1], nil
num--
i--
case *GraphNodeResourceProvider:
// Keep these around, but fix up the source to be ourselves
// rather than the old node.
newDep := *dep
newDep.Source = n
deps[i] = &newDep
default:
panic(fmt.Errorf("Unhandled depedency type: %#v", dep.Target.Meta))
} }
n.Deps = deps[:num]
} }
n.Deps = deps[:num]
} }
// Add the nouns to the graph // Add the nouns to the graph
@ -857,6 +910,10 @@ func graphAddModuleOrphans(
if n, err := graphModuleNoun(k, nil, g, opts); err != nil { if n, err := graphModuleNoun(k, nil, g, opts); err != nil {
return err return err
} else { } else {
// Mark this module as being an orphan
module := n.Meta.(*GraphNodeModule)
module.Flags |= FlagOrphan
module.State = m
nounsList = append(nounsList, n) nounsList = append(nounsList, n)
} }
} }
@ -916,6 +973,56 @@ func graphAddOrphanDeps(g *depgraph.Graph, mod *ModuleState) {
} }
} }
// graphAddOrphanModuleDeps adds the dependencies to the orphan
// modules based on their explicit Dependencies state.
func graphAddOrphanModuleDeps(g *depgraph.Graph, mod *ModuleState) {
for _, n := range g.Nouns {
module, ok := n.Meta.(*GraphNodeModule)
if !ok {
continue
}
if module.Flags&FlagOrphan == 0 {
continue
}
// If we have no dependencies, then just continue
if len(module.State.Dependencies) == 0 {
continue
}
for _, n2 := range g.Nouns {
// Don't ever depend on ourselves
if n2.Meta == n.Meta {
continue
}
var compareName string
switch rn2 := n2.Meta.(type) {
case *GraphNodeModule:
compareName = n2.Name
case *GraphNodeResource:
compareName = rn2.Resource.Id
}
if compareName == "" {
continue
}
for _, depName := range module.State.Dependencies {
if !strings.HasPrefix(depName, compareName) {
continue
}
dep := &depgraph.Dependency{
Name: depName,
Source: n,
Target: n2,
}
n.Deps = append(n.Deps, dep)
break
}
}
}
}
// graphAddOrphans adds the orphans to the graph. // graphAddOrphans adds the orphans to the graph.
func graphAddOrphans(g *depgraph.Graph, c *config.Config, mod *ModuleState) { func graphAddOrphans(g *depgraph.Graph, c *config.Config, mod *ModuleState) {
meta := g.Meta.(*GraphMeta) meta := g.Meta.(*GraphMeta)

View File

@ -731,6 +731,61 @@ func TestGraphAddDiff_module(t *testing.T) {
} }
} }
func TestGraphAddDiff_module_depends(t *testing.T) {
m := testModule(t, "graph-diff-module-dep")
diff := &Diff{
Modules: []*ModuleDiff{
&ModuleDiff{
Path: rootModulePath,
Resources: map[string]*InstanceDiff{
"aws_instance.foo": &InstanceDiff{
Destroy: true,
},
},
},
&ModuleDiff{
Path: []string{"root", "child"},
Destroy: true,
Resources: map[string]*InstanceDiff{
"aws_instance.foo": &InstanceDiff{
Destroy: true,
},
},
},
},
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: []string{"root", "orphan"},
Resources: map[string]*ResourceState{
"aws_instance.dead": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "dead",
},
},
},
Dependencies: []string{
"aws_instance.foo",
"module.child",
},
},
},
}
g, err := Graph(&GraphOpts{Module: m, Diff: diff, State: state})
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTerraformGraphDiffModuleDependsStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestGraphAddDiff_createBeforeDestroy(t *testing.T) { func TestGraphAddDiff_createBeforeDestroy(t *testing.T) {
m := testModule(t, "graph-diff-create-before") m := testModule(t, "graph-diff-create-before")
diff := &Diff{ diff := &Diff{
@ -909,7 +964,7 @@ func TestGraphEncodeDependencies_count(t *testing.T) {
func TestGraphEncodeDependencies_module(t *testing.T) { func TestGraphEncodeDependencies_module(t *testing.T) {
m := testModule(t, "graph-modules") m := testModule(t, "graph-modules")
g, err := Graph(&GraphOpts{Module: m}) g, err := Graph(&GraphOpts{Module: m, State: &State{}})
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
@ -928,6 +983,15 @@ func TestGraphEncodeDependencies_module(t *testing.T) {
if web.Dependencies[1] != "module.consul" { if web.Dependencies[1] != "module.consul" {
t.Fatalf("bad: %#v", web) t.Fatalf("bad: %#v", web)
} }
mod := g.Noun("module.consul").Meta.(*GraphNodeModule)
deps := mod.State.Dependencies
if len(deps) != 1 {
t.Fatalf("Bad: %#v", deps)
}
if deps[0] != "aws_security_group.firewall" {
t.Fatalf("Bad: %#v", deps)
}
} }
func TestGraph_orphan_dependencies(t *testing.T) { func TestGraph_orphan_dependencies(t *testing.T) {
@ -1012,6 +1076,57 @@ func TestGraph_orphanDependenciesModules(t *testing.T) {
} }
} }
func TestGraph_orphanModules_Dependencies(t *testing.T) {
m := testModule(t, "graph-modules")
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
Dependencies: []string{
"module.consul",
},
},
},
},
// Add an orphan module
&ModuleState{
Path: []string{"root", "orphan"},
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
},
},
},
Dependencies: []string{
"aws_instance.foo",
"aws_instance.web",
},
},
},
}
g, err := Graph(&GraphOpts{Module: m, State: state})
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTerraformGraphOrphanedModuleDepsStr)
if actual != expected {
t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\n%s", actual, expected)
}
}
func TestGraphNodeResourceExpand(t *testing.T) { func TestGraphNodeResourceExpand(t *testing.T) {
m := testModule(t, "graph-resource-expand") m := testModule(t, "graph-resource-expand")
@ -1214,6 +1329,22 @@ root
root -> module.child root -> module.child
` `
const testTerraformGraphDiffModuleDependsStr = `
root: root
aws_instance.foo
aws_instance.foo -> aws_instance.foo (destroy)
aws_instance.foo (destroy)
aws_instance.foo (destroy) -> module.child
aws_instance.foo (destroy) -> module.orphan
module.child
module.child -> module.orphan
module.orphan
root
root -> aws_instance.foo
root -> module.child
root -> module.orphan
`
const testTerraformGraphModulesStr = ` const testTerraformGraphModulesStr = `
root: root root: root
aws_instance.web aws_instance.web
@ -1382,6 +1513,33 @@ root
root -> module.consul root -> module.consul
` `
const testTerraformGraphOrphanedModuleDepsStr = `
root: root
aws_instance.foo
aws_instance.foo -> module.consul
aws_instance.foo -> provider.aws
aws_instance.web
aws_instance.web -> aws_security_group.firewall
aws_instance.web -> module.consul
aws_instance.web -> provider.aws
aws_security_group.firewall
aws_security_group.firewall -> provider.aws
module.consul
module.consul -> aws_security_group.firewall
module.consul -> provider.aws
module.orphan
module.orphan -> aws_instance.foo
module.orphan -> aws_instance.web
module.orphan -> provider.aws
provider.aws
root
root -> aws_instance.foo
root -> aws_instance.web
root -> aws_security_group.firewall
root -> module.consul
root -> module.orphan
`
const testTerraformGraphResourceExpandStr = ` const testTerraformGraphResourceExpandStr = `
root: root root: root
aws_instance.web.0 aws_instance.web.0

View File

@ -185,6 +185,20 @@ type ModuleState struct {
// N instances underneath, although a user only needs to think // N instances underneath, although a user only needs to think
// about the 1:1 case. // about the 1:1 case.
Resources map[string]*ResourceState `json:"resources"` Resources map[string]*ResourceState `json:"resources"`
// Dependencies are a list of things that this module relies on
// existing to remain intact. For example: an module may depend
// on a VPC ID given by an aws_vpc resource.
//
// Terraform uses this information to build valid destruction
// orders and to warn the user if they're destroying a module that
// another resource depends on.
//
// Things can be put into this list that may not be managed by
// Terraform. If Terraform doesn't find a matching ID in the
// overall state, then it assumes it isn't managed and doesn't
// worry about it.
Dependencies []string `json:"depends_on,omitempty"`
} }
// IsRoot says whether or not this module diff is for the root module. // IsRoot says whether or not this module diff is for the root module.

View File

@ -180,6 +180,9 @@ func TestReadWriteState(t *testing.T) {
Modules: []*ModuleState{ Modules: []*ModuleState{
&ModuleState{ &ModuleState{
Path: rootModulePath, Path: rootModulePath,
Dependencies: []string{
"aws_instance.bar",
},
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"foo": &ResourceState{ "foo": &ResourceState{
Primary: &InstanceState{ Primary: &InstanceState{

View File

@ -762,6 +762,7 @@ DIFF:
DESTROY: aws_instance.foo DESTROY: aws_instance.foo
module.child: module.child:
DESTROY MODULE
DESTROY: aws_instance.foo DESTROY: aws_instance.foo
STATE: STATE:

View File

@ -0,0 +1,5 @@
resource "aws_instance" "foo" {}
output "bar" {
value = "baz"
}

View File

@ -0,0 +1,8 @@
resource "aws_instance" "foo" {}
module "child" {
source = "./child"
in = "${aws_instance.foo.id}"
}