diff --git a/config/config.go b/config/config.go index 055a7f3306f6..876c909bc9e0 100644 --- a/config/config.go +++ b/config/config.go @@ -86,6 +86,8 @@ type Resource struct { Provider string DependsOn []string Lifecycle ResourceLifecycle + + DeferCountComputation bool } // Copy returns a copy of this Resource. Helpful for avoiding shared @@ -225,12 +227,20 @@ func (r *Resource) Count() (int, error) { raw := r.RawCount.Value() count, ok := r.RawCount.Value().(string) if !ok { + if r.DeferCountComputation { + return 1, nil + } + return 0, fmt.Errorf( "expected count to be a string or int, got %T", raw) } v, err := strconv.ParseInt(count, 0, 0) if err != nil { + if r.DeferCountComputation { + return 1, nil + } + return 0, err } @@ -590,18 +600,18 @@ func (c *Config) Validate() tfdiags.Diagnostics { "%s: resource count can't reference count variable: %s", n, v.FullKey(), )) - case *SimpleVariable: - diags = diags.Append(fmt.Errorf( - "%s: resource count can't reference variable: %s", - n, v.FullKey(), - )) - - // Good case *ModuleVariable: + // Good case *ResourceVariable: + // Good case *TerraformVariable: + // Good + case *SimpleVariable: + // Good case *UserVariable: + // Good case *LocalVariable: + // Good default: diags = diags.Append(fmt.Errorf( diff --git a/terraform/eval_context.go b/terraform/eval_context.go index 86481dedb4be..dc6365edf07b 100644 --- a/terraform/eval_context.go +++ b/terraform/eval_context.go @@ -86,4 +86,11 @@ type EvalContext interface { // State returns the global state as well as the lock that should // be used to modify that state. State() (*State, *sync.RWMutex) + + // True if we can ignore missing computed values when interpolating + // the count variable + // + // This is useful as we don't have the full tree during walkInput + // or walkValidate + CanIgnoreMissingCountExpansion() bool } diff --git a/terraform/eval_context_builtin.go b/terraform/eval_context_builtin.go index 1b6ee5a625e7..a3eac3f8bc85 100644 --- a/terraform/eval_context_builtin.go +++ b/terraform/eval_context_builtin.go @@ -41,6 +41,7 @@ type BuiltinEvalContext struct { DiffLock *sync.RWMutex StateValue *State StateLock *sync.RWMutex + IgnoreMissingCount bool once sync.Once } @@ -54,6 +55,10 @@ func (ctx *BuiltinEvalContext) Stopped() <-chan struct{} { return ctx.StopContext.Done() } +func (ctx *BuiltinEvalContext) CanIgnoreMissingCountExpansion() bool { + return ctx.IgnoreMissingCount +} + func (ctx *BuiltinEvalContext) Hook(fn func(Hook) (HookAction, error)) error { for _, h := range ctx.Hooks { action, err := fn(h) diff --git a/terraform/eval_context_mock.go b/terraform/eval_context_mock.go index 646451795dc5..4da6f16934be 100644 --- a/terraform/eval_context_mock.go +++ b/terraform/eval_context_mock.go @@ -198,3 +198,7 @@ func (c *MockEvalContext) State() (*State, *sync.RWMutex) { c.StateCalled = true return c.StateState, c.StateLock } + +func (c *MockEvalContext) CanIgnoreMissingCountExpansion() bool { + return false +} diff --git a/terraform/eval_count_computed.go b/terraform/eval_count_computed.go index 54a8333e0fc4..b7e941fb55c9 100644 --- a/terraform/eval_count_computed.go +++ b/terraform/eval_count_computed.go @@ -6,19 +6,26 @@ import ( "github.com/hashicorp/terraform/config" ) -// EvalCountCheckComputed is an EvalNode that checks if a resource count -// is computed and errors if so. This can possibly happen across a -// module boundary and we don't yet support this. -type EvalCountCheckComputed struct { +// EvalCountFixComputed is an EvalNode that checks if a resource count +// is computed and try to fix the value. +// It can only be fixed if the walk operation is either walkInput or walkValidate +// Otherwise it errors +type EvalCountFixComputed struct { Resource *config.Resource } // TODO: test -func (n *EvalCountCheckComputed) Eval(ctx EvalContext) (interface{}, error) { - if n.Resource.RawCount.Value() == unknownValue() { - return nil, fmt.Errorf( - "%s: value of 'count' cannot be computed", - n.Resource.Id()) +func (n *EvalCountFixComputed) Eval(ctx EvalContext) (interface{}, error) { + if ctx.CanIgnoreMissingCountExpansion() { + n.Resource.DeferCountComputation = true + } else { + n.Resource.DeferCountComputation = false + + if n.Resource.RawCount.Value() == unknownValue() { + return nil, fmt.Errorf( + "%s: value of 'count' cannot be computed", + n.Resource.Id()) + } } return nil, nil diff --git a/terraform/graph_walk_context.go b/terraform/graph_walk_context.go index 89f376e54f81..bec27295ffe2 100644 --- a/terraform/graph_walk_context.go +++ b/terraform/graph_walk_context.go @@ -65,6 +65,11 @@ func (w *ContextGraphWalker) EnterPath(path []string) EvalContext { w.interpolaterVars[key] = variables w.interpolaterVarLock.Unlock() + ignoreMissingCount := false + if w.Operation == walkInput || w.Operation == walkValidate || w.Operation == walkRefresh || w.Operation == walkPlan { + ignoreMissingCount = true + } + ctx := &BuiltinEvalContext{ StopContext: w.StopContext, PathValue: path, @@ -91,6 +96,7 @@ func (w *ContextGraphWalker) EnterPath(path []string) EvalContext { }, InterpolaterVars: w.interpolaterVars, InterpolaterVarLock: &w.interpolaterVarLock, + IgnoreMissingCount: ignoreMissingCount, } w.contexts[key] = ctx diff --git a/terraform/interpolate.go b/terraform/interpolate.go index 456e7e3a23c7..d529bb79cfeb 100644 --- a/terraform/interpolate.go +++ b/terraform/interpolate.go @@ -780,8 +780,9 @@ func (i *Interpolater) resourceCountMax( } count, err := cr.Count() + if err != nil { - return 0, err + return 1, nil } return count, nil diff --git a/terraform/node_resource_abstract_count.go b/terraform/node_resource_abstract_count.go index 573570d8e2ca..ea3b05557c49 100644 --- a/terraform/node_resource_abstract_count.go +++ b/terraform/node_resource_abstract_count.go @@ -19,7 +19,7 @@ func (n *NodeAbstractCountResource) EvalTree() EvalNode { // into more computed values. var evalCountCheckComputed EvalNode if !n.Validate { - evalCountCheckComputed = &EvalCountCheckComputed{Resource: n.Config} + evalCountCheckComputed = &EvalCountFixComputed{Resource: n.Config} } return &EvalSequence{