return known empty containers during plan

When looking up a resource during plan, we need to return an empty
container type when we're certain there are going to be no instances.
It's now more common to reference resources in a context that needs to
be known during plan (e.g. for_each), and always returning a DynamicVal
her would block plan from succeeding.
This commit is contained in:
James Bardin 2020-07-23 17:17:06 -04:00
parent 5c31add2fc
commit da644568a5
2 changed files with 88 additions and 7 deletions

View File

@ -6055,3 +6055,57 @@ data "test_data_source" "foo" {}
}
}
}
// for_each can reference a resource with 0 instances
func TestContext2Plan_scaleInForEach(t *testing.T) {
p := testProvider("test")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
m := testModuleInline(t, map[string]string{
"main.tf": `
locals {
m = {}
}
resource "test_instance" "a" {
for_each = local.m
}
resource "test_instance" "b" {
for_each = test_instance.a
}
`})
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_instance.a[0]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"a0"}`),
Dependencies: []addrs.ConfigResource{},
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_instance.b").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"b"}`),
Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.a")},
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
)
ctx := testContext2(t, &ContextOpts{
Config: m,
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
State: state,
})
_, diags := ctx.Plan()
assertNoErrors(t, diags)
}

View File

@ -2,6 +2,7 @@ package terraform
import (
"fmt"
"log"
"os"
"path/filepath"
"sync"
@ -619,13 +620,39 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc
rs := d.Evaluator.State.Resource(addr.Absolute(d.ModulePath))
if rs == nil {
// we must return DynamicVal so that both interpretations
// can proceed without generating errors, and we'll deal with this
// in a later step where more information is gathered.
// (In practice we should only end up here during the validate walk,
// since later walks should have at least partial states populated
// for all resources in the configuration.)
return cty.DynamicVal, diags
switch d.Operation {
case walkPlan:
// During plan as we evaluate each removed instance they are removed
// from the temporary working state. Since we know there there are
// no instances, and resources might be referenced in a context
// that needs to be known during plan, return an empty container of
// the expected type.
switch {
case config.Count != nil:
return cty.EmptyTupleVal, diags
case config.ForEach != nil:
return cty.EmptyObjectVal, diags
default:
// FIXME: try to prove this path should not be reached during plan.
//
// while we can reference an expanded resource with 0
// instances, we cannot reference instances that do not exist.
// Since we haven't ensured that all instances exist in all
// cases (this path only ever returned unknown), only log this as
// an error for now, and continue to return a DynamicVal
log.Printf("[ERROR] unknown instance %q referenced during plan", addr.Absolute(d.ModulePath))
return cty.DynamicVal, diags
}
default:
// we must return DynamicVal so that both interpretations
// can proceed without generating errors, and we'll deal with this
// in a later step where more information is gathered.
// (In practice we should only end up here during the validate walk,
// since later walks should have at least partial states populated
// for all resources in the configuration.)
return cty.DynamicVal, diags
}
}
providerAddr := rs.ProviderConfig