terraform: resources nested within a module must also be depended on

For example: A => B => C (modules). If A depends on module B, then it
also must depend on everything in module C.
This commit is contained in:
Mitchell Hashimoto 2016-11-12 15:38:28 -08:00
parent bcfec4e24e
commit 538302f143
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
9 changed files with 173 additions and 5 deletions

View File

@ -335,6 +335,114 @@ module.child:
}
}
func TestContext2Apply_resourceDependsOnModuleGrandchild(t *testing.T) {
m := testModule(t, "apply-resource-depends-on-module-deep")
p := testProvider("aws")
p.DiffFn = testDiffFn
{
// Wait for the dependency, sleep, and verify the graph never
// called a child.
var called int32
var checked bool
p.ApplyFn = func(
info *InstanceInfo,
is *InstanceState,
id *InstanceDiff) (*InstanceState, error) {
if info.HumanId() == "module.child.grandchild.aws_instance.c" {
checked = true
// Sleep to allow parallel execution
time.Sleep(50 * time.Millisecond)
// Verify that called is 0 (dep not called)
if atomic.LoadInt32(&called) != 0 {
return nil, fmt.Errorf("aws_instance.a should not be called")
}
}
atomic.AddInt32(&called, 1)
return testApplyFn(info, is, id)
}
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
})
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
if !checked {
t.Fatal("should check")
}
checkStateString(t, state, testTerraformApplyResourceDependsOnModuleDeepStr)
}
}
func TestContext2Apply_resourceDependsOnModuleInModule(t *testing.T) {
m := testModule(t, "apply-resource-depends-on-module-in-module")
p := testProvider("aws")
p.DiffFn = testDiffFn
{
// Wait for the dependency, sleep, and verify the graph never
// called a child.
var called int32
var checked bool
p.ApplyFn = func(
info *InstanceInfo,
is *InstanceState,
id *InstanceDiff) (*InstanceState, error) {
if info.HumanId() == "module.child.grandchild.aws_instance.c" {
checked = true
// Sleep to allow parallel execution
time.Sleep(50 * time.Millisecond)
// Verify that called is 0 (dep not called)
if atomic.LoadInt32(&called) != 0 {
return nil, fmt.Errorf("nothing else should not be called")
}
}
atomic.AddInt32(&called, 1)
return testApplyFn(info, is, id)
}
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
})
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
if !checked {
t.Fatal("should check")
}
checkStateString(t, state, testTerraformApplyResourceDependsOnModuleInModuleStr)
}
}
func TestContext2Apply_mapVarBetweenModules(t *testing.T) {
m := testModule(t, "apply-map-var-through-module")
p := testProvider("null")

View File

@ -665,6 +665,31 @@ module.child:
ID = foo
`
const testTerraformApplyResourceDependsOnModuleDeepStr = `
aws_instance.a:
ID = foo
Dependencies:
module.child
module.child.grandchild:
aws_instance.c:
ID = foo
`
const testTerraformApplyResourceDependsOnModuleInModuleStr = `
<no state>
module.child:
aws_instance.b:
ID = foo
Dependencies:
module.grandchild
module.child.grandchild:
aws_instance.c:
ID = foo
`
const testTerraformApplyTaintStr = `
aws_instance.bar:
ID = foo

View File

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

View File

@ -0,0 +1,3 @@
module "grandchild" {
source = "./child"
}

View File

@ -0,0 +1,7 @@
module "child" {
source = "./child"
}
resource "aws_instance" "a" {
depends_on = ["module.child"]
}

View File

@ -0,0 +1,2 @@
resource "aws_instance" "c" {}

View File

@ -0,0 +1,7 @@
module "grandchild" {
source = "./child"
}
resource "aws_instance" "b" {
depends_on = ["module.grandchild"]
}

View File

@ -0,0 +1,3 @@
module "child" {
source = "./child"
}

View File

@ -11,6 +11,11 @@ import (
// GraphNodeReferenceable must be implemented by any node that represents
// a Terraform thing that can be referenced (resource, module, etc.).
//
// Even if the thing has no name, this should return an empty list. By
// implementing this and returning a non-nil result, you say that this CAN
// be referenced and other methods of referencing may still be possible (such
// as by path!)
type GraphNodeReferenceable interface {
// ReferenceableName is the name by which this can be referenced.
// This can be either just the type, or include the field. Example:
@ -205,7 +210,7 @@ func NewReferenceMap(vs []dag.Vertex) *ReferenceMap {
// example, if this is a referenceable thing at path []string{"foo"},
// then it can be referenced at "module.foo"
if pn, ok := v.(GraphNodeSubPath); ok {
if p := ReferenceModulePath(pn.Path()); p != "" {
for _, p := range ReferenceModulePath(pn.Path()) {
refMap[p] = append(refMap[p], v)
}
}
@ -234,15 +239,22 @@ func NewReferenceMap(vs []dag.Vertex) *ReferenceMap {
}
// Returns the reference name for a module path. The path "foo" would return
// "module.foo"
func ReferenceModulePath(p []string) string {
// "module.foo". If this is a deeply nested module, it will be every parent
// as well. For example: ["foo", "bar"] would return both "module.foo" and
// "module.foo.module.bar"
func ReferenceModulePath(p []string) []string {
p = normalizeModulePath(p)
if len(p) == 1 {
// Root, no name
return ""
return nil
}
return fmt.Sprintf("module.%s", strings.Join(p[1:], "."))
result := make([]string, 0, len(p)-1)
for i := len(p); i > 1; i-- {
result = append(result, modulePrefixStr(p[:i]))
}
return result
}
// ReferencesFromConfig returns the references that a configuration has