diff --git a/terraform/interpolate.go b/terraform/interpolate.go index 11eb193b6..cb19d4eb6 100644 --- a/terraform/interpolate.go +++ b/terraform/interpolate.go @@ -230,26 +230,38 @@ func (i *Interpolater) valueResourceVar( return nil } + var variable *ast.Variable + var err error + if v.Multi && v.Index == -1 { - variable, err := i.computeResourceMultiVariable(scope, v) - if err != nil { - return err - } - if variable == nil { - return fmt.Errorf("no error reported by variable %q is nil", v.Name) - } - result[n] = *variable + variable, err = i.computeResourceMultiVariable(scope, v) } else { - variable, err := i.computeResourceVariable(scope, v) - if err != nil { - return err - } - if variable == nil { - return fmt.Errorf("no error reported by variable %q is nil", v.Name) - } - result[n] = *variable + variable, err = i.computeResourceVariable(scope, v) } + if err != nil { + return err + } + + if variable == nil { + // During the input walk we tolerate missing variables because + // we haven't yet had a chance to refresh state, so dynamic data may + // not yet be complete. + // If it truly is missing, we'll catch it on a later walk. + // This applies only to graph nodes that interpolate during the + // config walk, e.g. providers. + if i.Operation == walkInput { + result[n] = ast.Variable{ + Value: config.UnknownVariableValue, + Type: ast.TypeString, + } + return nil + } + + return fmt.Errorf("variable %q is nil, but no error was reported", v.Name) + } + + result[n] = *variable return nil } diff --git a/terraform/interpolate_test.go b/terraform/interpolate_test.go index e3777ae4a..9485512a2 100644 --- a/terraform/interpolate_test.go +++ b/terraform/interpolate_test.go @@ -174,6 +174,60 @@ func TestInterpolater_resourceVariable(t *testing.T) { }) } +func TestInterpolater_resourceVariableMissingDuringInput(t *testing.T) { + // During the input walk, computed resource attributes may be entirely + // absent since we've not yet produced diffs that tell us what computed + // attributes to expect. In that case, interpolator tolerates it and + // indicates the value is computed. + + lock := new(sync.RWMutex) + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + // No resources at all yet, because we're still dealing + // with input and so the resources haven't been created. + }, + }, + }, + } + + { + i := &Interpolater{ + Operation: walkInput, + Module: testModule(t, "interpolate-resource-variable"), + State: state, + StateLock: lock, + } + + scope := &InterpolationScope{ + Path: rootModulePath, + } + + testInterpolate(t, i, scope, "aws_instance.web.foo", ast.Variable{ + Value: config.UnknownVariableValue, + Type: ast.TypeString, + }) + } + + // This doesn't apply during other walks, like plan + { + i := &Interpolater{ + Operation: walkPlan, + Module: testModule(t, "interpolate-resource-variable"), + State: state, + StateLock: lock, + } + + scope := &InterpolationScope{ + Path: rootModulePath, + } + + testInterpolateErr(t, i, scope, "aws_instance.web.foo") + } +} + func TestInterpolater_resourceVariableMulti(t *testing.T) { lock := new(sync.RWMutex) state := &State{ @@ -473,3 +527,20 @@ func testInterpolate( t.Fatalf("%q: actual: %#v\nexpected: %#v", n, actual, expected) } } + +func testInterpolateErr( + t *testing.T, i *Interpolater, + scope *InterpolationScope, + n string) { + v, err := config.NewInterpolatedVariable(n) + if err != nil { + t.Fatalf("err: %s", err) + } + + _, err = i.Values(scope, map[string]config.InterpolatedVariable{ + "foo": v, + }) + if err == nil { + t.Fatalf("%q: succeeded, but wanted error", n) + } +}