core: don't crash if no module state exists for multi var

For child modules, a ModuleState isn't allocated until the first time a
module instance is inserted into the state under the module's path.
Normally interpolations of resource attributes are delayed until at least
one resource has been created due to the nature of the dependency graph,
but if the interpolation value is a multi-var (splat) then it is possible
that the referenced resource has count=0 and thus created _no_ resource
states when it was visited.

Previously we would crash when trying to access the resource map for the
nil module in order to count how many instances are present. Since we know
there can't be any instances present in a nil module, we now preempt
this crash by returning zero early.

This edge-case does not apply to the root module because its ModuleState
is allocated as part of initializing the main State instance.

This fixes #14438.
This commit is contained in:
Martin Atkins 2017-05-15 15:40:29 -07:00
parent 055c18e302
commit 45b04c826a
5 changed files with 93 additions and 0 deletions

View File

@ -3447,6 +3447,36 @@ func TestContext2Apply_multiVarCountDec(t *testing.T) {
} }
} }
// Test that we can resolve a multi-var (splat) for the first resource
// created in a non-root module, which happens when the module state doesn't
// exist yet.
// https://github.com/hashicorp/terraform/issues/14438
func TestContext2Apply_multiVarMissingState(t *testing.T) {
m := testModule(t, "apply-multi-var-missing-state")
p := testProvider("test")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
// First, apply with a count of 3
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"test": testProviderFuncFixed(p),
},
})
if _, err := ctx.Plan(); err != nil {
t.Fatalf("plan failed: %s", err)
}
// Before the relevant bug was fixed, Terraform would panic during apply.
if _, err := ctx.Apply(); err != nil {
t.Fatalf("apply failed: %s", err)
}
// If we get here with no errors or panics then our test was successful.
}
func TestContext2Apply_nilDiff(t *testing.T) { func TestContext2Apply_nilDiff(t *testing.T) {
m := testModule(t, "apply-good") m := testModule(t, "apply-good")
p := testProvider("aws") p := testProvider("aws")

View File

@ -731,6 +731,19 @@ func (i *Interpolater) resourceCountMax(
return count, nil return count, nil
} }
// If we have no module state in the apply walk, that suggests we've hit
// a rather awkward edge-case: the resource this variable refers to
// has count = 0 and is the only resource processed so far on this walk,
// and so we've ended up not creating any resource states yet. We don't
// create a module state until the first resource is written into it,
// so the module state doesn't exist when we get here.
//
// In this case we act as we would if we had been passed a module
// with an empty resource state map.
if ms == nil {
return 0, nil
}
// We need to determine the list of resource keys to get values from. // We need to determine the list of resource keys to get values from.
// This needs to be sorted so the order is deterministic. We used to // This needs to be sorted so the order is deterministic. We used to
// use "cr.Count()" but that doesn't work if the count is interpolated // use "cr.Count()" but that doesn't work if the count is interpolated

View File

@ -437,6 +437,34 @@ func TestInterpolater_resourceVariableMultiPartialUnknown(t *testing.T) {
}) })
} }
func TestInterpolater_resourceVariableMultiNoState(t *testing.T) {
// When evaluating a "splat" variable in a module that doesn't have
// any state yet, we should still be able to resolve to an empty
// list.
// See https://github.com/hashicorp/terraform/issues/14438 for an
// example of what we're testing for here.
lock := new(sync.RWMutex)
state := &State{
Modules: []*ModuleState{},
}
i := &Interpolater{
Module: testModule(t, "interpolate-resource-variable-multi"),
State: state,
StateLock: lock,
Operation: walkApply,
}
scope := &InterpolationScope{
Path: rootModulePath,
}
testInterpolate(t, i, scope, "aws_instance.web.*.foo", ast.Variable{
Type: ast.TypeList,
Value: []ast.Variable{},
})
}
// When a splat reference is made to an attribute that is a computed list, // When a splat reference is made to an attribute that is a computed list,
// the result should be unknown. // the result should be unknown.
func TestInterpolater_resourceVariableMultiList(t *testing.T) { func TestInterpolater_resourceVariableMultiList(t *testing.T) {

View File

@ -0,0 +1,15 @@
# This resource gets visited first on the apply walk, but since it DynamicExpands
# to an empty subgraph it ends up being a no-op, leaving the module state
# uninitialized.
resource "test_thing" "a" {
count = 0
}
# This resource is visited second. During its eval walk we try to build the
# array for the null_resource.a.*.id interpolation, which involves iterating
# over all of the resource in the state. This should succeed even though the
# module state will be nil when evaluating the variable.
resource "test_thing" "b" {
a_ids = "${join(" ", null_resource.a.*.id)}"
}

View File

@ -0,0 +1,7 @@
// We test this in a child module, since the root module state exists
// very early on, even before any resources are created in it, but that is not
// true for child modules.
module "child" {
source = "./child"
}