diff --git a/internal/experiments/experiment.go b/internal/experiments/experiment.go index 3164939b0a8f..22365f51e6b1 100644 --- a/internal/experiments/experiment.go +++ b/internal/experiments/experiment.go @@ -21,11 +21,13 @@ const ( SuppressProviderSensitiveAttrs = Experiment("provider_sensitive_attrs") ConfigDrivenMove = Experiment("config_driven_move") PreconditionsPostconditions = Experiment("preconditions_postconditions") + UnknownInstances = Experiment("unknown_instances") ) func init() { // Each experiment constant defined above must be registered here as either // a current or a concluded experiment. + registerCurrentExperiment(UnknownInstances) registerConcludedExperiment(VariableValidation, "Custom variable validation can now be used by default, without enabling an experiment.") registerConcludedExperiment(SuppressProviderSensitiveAttrs, "Provider-defined sensitive attributes are now redacted by default, without enabling an experiment.") registerConcludedExperiment(ConfigDrivenMove, "Declarations of moved resource instances using \"moved\" blocks can now be used by default, without enabling an experiment.") diff --git a/internal/terraform/eval_context.go b/internal/terraform/eval_context.go index 594e07a96d49..5ab46f79a4d2 100644 --- a/internal/terraform/eval_context.go +++ b/internal/terraform/eval_context.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/moduletest/mocking" @@ -134,6 +135,11 @@ type EvalContext interface { // addresses in this context. EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope + // LanguageExperimentActive returns true if the given experiment is + // active in the module associated with this EvalContext, or false + // otherwise. + LanguageExperimentActive(experiment experiments.Experiment) bool + // NamedValues returns the object that tracks the gradual evaluation of // all input variables, local values, and output values during a graph // walk. diff --git a/internal/terraform/eval_context_builtin.go b/internal/terraform/eval_context_builtin.go index b9e718d39747..08d59384a425 100644 --- a/internal/terraform/eval_context_builtin.go +++ b/internal/terraform/eval_context_builtin.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/moduletest/mocking" @@ -536,6 +537,24 @@ func (ctx *BuiltinEvalContext) Path() addrs.ModuleInstance { return ctx.PathValue } +func (ctx *BuiltinEvalContext) LanguageExperimentActive(experiment experiments.Experiment) bool { + if ctx.Evaluator == nil || ctx.Evaluator.Config == nil { + // Should not get here in normal code, but might get here in test code + // if the context isn't fully populated. + return false + } + if !ctx.pathSet { + // An EvalContext that isn't associated with a module path cannot + // have active experiments. + return false + } + cfg := ctx.Evaluator.Config.DescendentForInstance(ctx.Path()) + if cfg == nil { + return false + } + return cfg.Module.ActiveExperiments.Has(experiment) +} + func (ctx *BuiltinEvalContext) NamedValues() *namedvals.State { return ctx.NamedValuesValue } diff --git a/internal/terraform/eval_context_mock.go b/internal/terraform/eval_context_mock.go index 401a96f1ee8c..89f31b5dd9b9 100644 --- a/internal/terraform/eval_context_mock.go +++ b/internal/terraform/eval_context_mock.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/moduletest/mocking" @@ -119,6 +120,8 @@ type MockEvalContext struct { PathCalled bool PathPath addrs.ModuleInstance + LanguageExperimentsActive experiments.Set + NamedValuesCalled bool NamedValuesState *namedvals.State @@ -334,6 +337,15 @@ func (c *MockEvalContext) Path() addrs.ModuleInstance { return c.PathPath } +func (c *MockEvalContext) LanguageExperimentActive(experiment experiments.Experiment) bool { + // This particular function uses a live data structure so that tests can + // exercise different experiments being enabled; there is little reason + // to directly test whether this function was called since we use this + // function only temporarily while an experiment is active, and then + // remove the calls once the experiment is concluded. + return c.LanguageExperimentsActive.Has(experiment) +} + func (c *MockEvalContext) NamedValues() *namedvals.State { c.NamedValuesCalled = true return c.NamedValuesState diff --git a/internal/terraform/eval_count.go b/internal/terraform/eval_count.go index 9e2144d9e0f6..3d1f091bf4e1 100644 --- a/internal/terraform/eval_count.go +++ b/internal/terraform/eval_count.go @@ -6,10 +6,11 @@ package terraform import ( "fmt" - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/gocty" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/tfdiags" ) // evaluateCountExpression is our standard mechanism for interpreting an @@ -20,9 +21,15 @@ import ( // evaluateCountExpression differs from evaluateCountExpressionValue by // returning an error if the count value is not known, and converting the // cty.Value to an integer. -func evaluateCountExpression(expr hcl.Expression, ctx EvalContext) (int, tfdiags.Diagnostics) { +// +// If allowUnknown is false then this function will return error diagnostics +// whenever the expression returns an unknown value. Setting allowUnknown to +// true instead permits unknown values, indicating them by returning the +// placeholder value -1. Callers can assume that a return value of -1 without +// any error diagnostics represents a valid unknown value. +func evaluateCountExpression(expr hcl.Expression, ctx EvalContext, allowUnknown bool) (int, tfdiags.Diagnostics) { countVal, diags := evaluateCountExpressionValue(expr, ctx) - if !countVal.IsKnown() { + if !allowUnknown && !countVal.IsKnown() { // Currently this is a rather bad outcome from a UX standpoint, since we have // no real mechanism to deal with this situation and all we can do is produce // an error message. diff --git a/internal/terraform/eval_count_test.go b/internal/terraform/eval_count_test.go index 74bcfbdf721d..d4d785c37dc6 100644 --- a/internal/terraform/eval_count_test.go +++ b/internal/terraform/eval_count_test.go @@ -32,7 +32,7 @@ func TestEvaluateCountExpression(t *testing.T) { t.Run(name, func(t *testing.T) { ctx := &MockEvalContext{} ctx.installSimpleEval() - countVal, diags := evaluateCountExpression(test.Expr, ctx) + countVal, diags := evaluateCountExpression(test.Expr, ctx, false) if len(diags) != 0 { t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) @@ -47,3 +47,37 @@ func TestEvaluateCountExpression(t *testing.T) { }) } } + +func TestEvaluateCountExpression_allowUnknown(t *testing.T) { + tests := map[string]struct { + Expr hcl.Expression + Count int + }{ + "unknown number": { + hcltest.MockExprLiteral(cty.UnknownVal(cty.Number)), + -1, + }, + "dynamicval": { + hcltest.MockExprLiteral(cty.DynamicVal), + -1, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ctx := &MockEvalContext{} + ctx.installSimpleEval() + countVal, diags := evaluateCountExpression(test.Expr, ctx, true) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) + } + + if !reflect.DeepEqual(countVal, test.Count) { + t.Errorf( + "wrong result\ngot: %#v\nwant: %#v", + countVal, test.Count, + ) + } + }) + } +} diff --git a/internal/terraform/eval_for_each.go b/internal/terraform/eval_for_each.go index 3782234e9b0e..c567a543970f 100644 --- a/internal/terraform/eval_for_each.go +++ b/internal/terraform/eval_for_each.go @@ -19,20 +19,21 @@ import ( // evaluateForEachExpression differs from evaluateForEachExpressionValue by // returning an error if the count value is not known, and converting the // cty.Value to a map[string]cty.Value for compatibility with other calls. -func evaluateForEachExpression(expr hcl.Expression, ctx EvalContext) (forEach map[string]cty.Value, diags tfdiags.Diagnostics) { - return newForEachEvaluator(expr, ctx).ResourceValue() +func evaluateForEachExpression(expr hcl.Expression, ctx EvalContext, allowUnknown bool) (forEach map[string]cty.Value, known bool, diags tfdiags.Diagnostics) { + return newForEachEvaluator(expr, ctx, allowUnknown).ResourceValue() } // forEachEvaluator is the standard mechanism for interpreting an expression // given for a "for_each" argument on a resource, module, or import. -func newForEachEvaluator(expr hcl.Expression, ctx EvalContext) *forEachEvaluator { +func newForEachEvaluator(expr hcl.Expression, ctx EvalContext, allowUnknown bool) *forEachEvaluator { if ctx == nil { panic("nil EvalContext") } return &forEachEvaluator{ - ctx: ctx, - expr: expr, + ctx: ctx, + expr: expr, + allowUnknown: allowUnknown, } } @@ -48,44 +49,54 @@ type forEachEvaluator struct { ctx EvalContext expr hcl.Expression + // TEMP: If allowUnknown is set then we skip the usual restriction that + // unknown values are not allowed in for_each. A caller that sets this + // must therefore be ready to deal with the result being unknown. + // This will eventually become the default behavior, once we've updated + // the rest of this package to handle that situation in a reasonable way. + allowUnknown bool + // internal hclCtx *hcl.EvalContext } // ResourceForEachValue returns a known for_each map[string]cty.Value // appropriate for use within resource expansion. -func (ev *forEachEvaluator) ResourceValue() (map[string]cty.Value, tfdiags.Diagnostics) { +func (ev *forEachEvaluator) ResourceValue() (map[string]cty.Value, bool, tfdiags.Diagnostics) { res := map[string]cty.Value{} // no expression always results in an empty map if ev.expr == nil { - return res, nil + return res, true, nil } forEachVal, diags := ev.Value() if diags.HasErrors() { - return res, diags + return res, false, diags } // ensure our value is known for use in resource expansion - diags = diags.Append(ev.ensureKnownForResource(forEachVal)) - if diags.HasErrors() { - return res, diags + unknownDiags := ev.ensureKnownForResource(forEachVal) + if unknownDiags.HasErrors() { + if !ev.allowUnknown { + diags = diags.Append(unknownDiags) + } + return res, false, diags } // validate the for_each value for use in resource expansion diags = diags.Append(ev.validateResource(forEachVal)) if diags.HasErrors() { - return res, diags + return res, false, diags } if forEachVal.IsNull() || !forEachVal.IsKnown() || markSafeLengthInt(forEachVal) == 0 { // we check length, because an empty set returns a nil map which will panic below - return res, diags + return res, true, diags } res = forEachVal.AsValueMap() - return res, diags + return res, true, diags } // ImportValue returns the for_each map for use within an import block, diff --git a/internal/terraform/eval_for_each_test.go b/internal/terraform/eval_for_each_test.go index 3f7717d09015..6f4885b1d489 100644 --- a/internal/terraform/eval_for_each_test.go +++ b/internal/terraform/eval_for_each_test.go @@ -72,7 +72,7 @@ func TestEvaluateForEachExpression_valid(t *testing.T) { t.Run(name, func(t *testing.T) { ctx := &MockEvalContext{} ctx.installSimpleEval() - forEachMap, diags := evaluateForEachExpression(test.Expr, ctx) + forEachMap, _, diags := evaluateForEachExpression(test.Expr, ctx, false) if len(diags) != 0 { t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) @@ -176,7 +176,7 @@ func TestEvaluateForEachExpression_errors(t *testing.T) { t.Run(name, func(t *testing.T) { ctx := &MockEvalContext{} ctx.installSimpleEval() - _, diags := evaluateForEachExpression(test.Expr, ctx) + _, _, diags := evaluateForEachExpression(test.Expr, ctx, false) if len(diags) != 1 { t.Fatalf("got %d diagnostics; want 1", len(diags)) @@ -211,6 +211,41 @@ func TestEvaluateForEachExpression_errors(t *testing.T) { } } +func TestEvaluateForEachExpression_allowUnknown(t *testing.T) { + tests := map[string]struct { + Expr hcl.Expression + }{ + "unknown string set": { + hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))), + }, + "unknown map": { + hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.Bool))), + }, + "set containing unknown value": { + hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.UnknownVal(cty.String)})), + }, + "set containing dynamic unknown value": { + hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.UnknownVal(cty.DynamicPseudoType)})), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ctx := &MockEvalContext{} + ctx.installSimpleEval() + _, known, diags := evaluateForEachExpression(test.Expr, ctx, true) + + // With allowUnknown set, all of these expressions should be treated + // as valid for_each values. + assertNoDiagnostics(t, diags) + + if known { + t.Errorf("result is known; want unknown") + } + }) + } +} + func TestEvaluateForEachExpressionKnown(t *testing.T) { tests := map[string]hcl.Expression{ "unknown string set": hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))), @@ -221,7 +256,7 @@ func TestEvaluateForEachExpressionKnown(t *testing.T) { t.Run(name, func(t *testing.T) { ctx := &MockEvalContext{} ctx.installSimpleEval() - diags := newForEachEvaluator(expr, ctx).ValidateResourceValue() + diags := newForEachEvaluator(expr, ctx, false).ValidateResourceValue() if len(diags) != 0 { t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) diff --git a/internal/terraform/instance_expanders.go b/internal/terraform/instance_expanders.go index 615f3910e34d..d77543aa47cf 100644 --- a/internal/terraform/instance_expanders.go +++ b/internal/terraform/instance_expanders.go @@ -3,8 +3,44 @@ package terraform +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/instances" +) + // graphNodeExpandsInstances is implemented by nodes that causes instances to // be registered in the instances.Expander. type graphNodeExpandsInstances interface { expandsInstances() } + +// forEachModuleInstance is a helper to deal with the common need of doing +// some action for every dynamic module instance associated with a static +// module path. +// +// Many of our plan graph nodes represent configuration constructs that need +// to produce a dynamic subgraph based on the expansion of whatever module +// they are declared inside, and this helper deals with enumerating those +// dynamic addresses so that callers can just focus on building a graph node +// for each one and registering it in the subgraph. +// +// Both of the two callbacks will be called for each instance or set of +// unknown instances. knownCb receives fully-known instance addresses, +// while unknownCb receives partially-expanded addresses. Callers typically +// create a different graph node type in each callback, because +// partially-expanded prefixes conceptually represent an infinite set of +// possible module instance addresses and therefore need quite different +// treatment than a single concrete module instance address. +func forEachModuleInstance( + insts *instances.Expander, + modAddr addrs.Module, + knownCb func(addrs.ModuleInstance), + unknownCb func(addrs.PartialExpandedModule), +) { + for _, instAddr := range insts.ExpandModule(modAddr) { + knownCb(instAddr) + } + for _, instsAddr := range insts.UnknownModuleInstances(modAddr) { + unknownCb(instsAddr) + } +} diff --git a/internal/terraform/node_check.go b/internal/terraform/node_check.go index 6e0c4ed6408d..97fbfde3ad87 100644 --- a/internal/terraform/node_check.go +++ b/internal/terraform/node_check.go @@ -84,14 +84,21 @@ func (n *nodeExpandCheck) ModulePath() addrs.Module { func (n *nodeExpandCheck) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagnostics) { exp := ctx.InstanceExpander() - modInsts := exp.ExpandModule(n.ModulePath()) var g Graph - for _, modAddr := range modInsts { - testAddr := n.addr.Check.Absolute(modAddr) - log.Printf("[TRACE] nodeExpandCheck: Node for %s", testAddr) - g.Add(n.makeInstance(testAddr, n.config)) - } + forEachModuleInstance( + exp, n.ModulePath(), + func(modAddr addrs.ModuleInstance) { + testAddr := n.addr.Check.Absolute(modAddr) + log.Printf("[TRACE] nodeExpandCheck: Node for %s", testAddr) + g.Add(n.makeInstance(testAddr, n.config)) + }, + func(pem addrs.PartialExpandedModule) { + // TODO: Graph node to check the placeholder values for all possible module instances in this prefix. + testAddr := addrs.ObjectInPartialExpandedModule(pem, n.addr) + log.Printf("[WARN] nodeExpandCheck: not yet doing placeholder-check for all %s", testAddr) + }, + ) addRootNodeToGraph(&g) return &g, nil diff --git a/internal/terraform/node_local.go b/internal/terraform/node_local.go index ad5943cab95f..defd5fc10a21 100644 --- a/internal/terraform/node_local.go +++ b/internal/terraform/node_local.go @@ -69,14 +69,25 @@ func (n *nodeExpandLocal) References() []*addrs.Reference { func (n *nodeExpandLocal) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagnostics) { var g Graph expander := ctx.InstanceExpander() - for _, module := range expander.ExpandModule(n.Module) { - o := &NodeLocal{ - Addr: n.Addr.Absolute(module), - Config: n.Config, - } - log.Printf("[TRACE] Expanding local: adding %s as %T", o.Addr.String(), o) - g.Add(o) - } + forEachModuleInstance( + expander, n.Module, + func(module addrs.ModuleInstance) { + o := &NodeLocal{ + Addr: n.Addr.Absolute(module), + Config: n.Config, + } + log.Printf("[TRACE] Expanding local: adding %s as %T", o.Addr.String(), o) + g.Add(o) + }, + func(pem addrs.PartialExpandedModule) { + o := &nodeLocalInPartialModule{ + Addr: addrs.ObjectInPartialExpandedModule(pem, n.Addr), + Config: n.Config, + } + log.Printf("[TRACE] Expanding local: adding placeholder for all %s as %T", o.Addr.String(), o) + g.Add(o) + }, + ) addRootNodeToGraph(&g) return &g, nil } @@ -176,3 +187,18 @@ func (n *NodeLocal) DotNode(name string, opts *dag.DotOpts) *dag.DotNode { }, } } + +// nodeLocalInPartialModule represents an infinite set of possible local value +// instances beneath a partially-expanded module instance prefix. +// +// Its job is to find a suitable placeholder value that approximates the +// values of all of those possible instances. Ideally that's a concrete +// known value if all instances would have the same value, an unknown value +// of a specific type if the definition produces a known type, or a +// totally-unknown value of unknown type in the worst case. +type nodeLocalInPartialModule struct { + Addr addrs.InPartialExpandedModule[addrs.LocalValue] + Config *configs.Local +} + +// TODO: Implement nodeLocalUnexpandedPlaceholder.Execute diff --git a/internal/terraform/node_module_expand.go b/internal/terraform/node_module_expand.go index cb6f3a88bca1..f9cb8f46b37d 100644 --- a/internal/terraform/node_module_expand.go +++ b/internal/terraform/node_module_expand.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -106,6 +107,15 @@ func (n *nodeExpandModule) Execute(ctx EvalContext, op walkOperation) (diags tfd expander := ctx.InstanceExpander() _, call := n.Addr.Call() + // Allowing unknown values in count and for_each is currently only an + // experimental feature. This will hopefully become the default (and only) + // behavior in future, if the experiment is successful. + // + // If this is false then the codepaths that handle unknown values below + // become unreachable, because the evaluate functions will reject unknown + // values as an error. + allowUnknown := ctx.LanguageExperimentActive(experiments.UnknownInstances) + // nodeExpandModule itself does not have visibility into how its ancestors // were expanded, so we use the expander here to provide all possible paths // to our module, and register module instances with each of them. @@ -113,20 +123,29 @@ func (n *nodeExpandModule) Execute(ctx EvalContext, op walkOperation) (diags tfd ctx = ctx.WithPath(module) switch { case n.ModuleCall.Count != nil: - count, ctDiags := evaluateCountExpression(n.ModuleCall.Count, ctx) + count, ctDiags := evaluateCountExpression(n.ModuleCall.Count, ctx, allowUnknown) diags = diags.Append(ctDiags) if diags.HasErrors() { return diags } - expander.SetModuleCount(module, call, count) + if count >= 0 { + expander.SetModuleCount(module, call, count) + } else { + // -1 represents "unknown" + expander.SetModuleCountUnknown(module, call) + } case n.ModuleCall.ForEach != nil: - forEach, feDiags := evaluateForEachExpression(n.ModuleCall.ForEach, ctx) + forEach, known, feDiags := evaluateForEachExpression(n.ModuleCall.ForEach, ctx, allowUnknown) diags = diags.Append(feDiags) if diags.HasErrors() { return diags } - expander.SetModuleForEach(module, call, forEach) + if known { + expander.SetModuleForEach(module, call, forEach) + } else { + expander.SetModuleForEachUnknown(module, call) + } default: expander.SetModuleSingle(module, call) @@ -256,7 +275,7 @@ func (n *nodeValidateModule) Execute(ctx EvalContext, op walkOperation) (diags t diags = diags.Append(countDiags) case n.ModuleCall.ForEach != nil: - forEachDiags := newForEachEvaluator(n.ModuleCall.ForEach, ctx).ValidateResourceValue() + forEachDiags := newForEachEvaluator(n.ModuleCall.ForEach, ctx, false).ValidateResourceValue() diags = diags.Append(forEachDiags) } diff --git a/internal/terraform/node_module_variable.go b/internal/terraform/node_module_variable.go index ac63b6860064..93c23a562a3b 100644 --- a/internal/terraform/node_module_variable.go +++ b/internal/terraform/node_module_variable.go @@ -65,21 +65,35 @@ func (n *nodeExpandModuleVariable) DynamicExpand(ctx EvalContext) (*Graph, tfdia } expander := ctx.InstanceExpander() - for _, module := range expander.ExpandModule(n.Module) { - addr := n.Addr.Absolute(module) - if checkableAddrs != nil { - checkableAddrs.Add(addr) - } + forEachModuleInstance( + expander, n.Module, + func(module addrs.ModuleInstance) { + addr := n.Addr.Absolute(module) + if checkableAddrs != nil { + checkableAddrs.Add(addr) + } - o := &nodeModuleVariable{ - Addr: addr, - Config: n.Config, - Expr: n.Expr, - ModuleInstance: module, - DestroyApply: n.DestroyApply, - } - g.Add(o) - } + o := &nodeModuleVariable{ + Addr: addr, + Config: n.Config, + Expr: n.Expr, + ModuleInstance: module, + DestroyApply: n.DestroyApply, + } + g.Add(o) + }, + func(pem addrs.PartialExpandedModule) { + addr := addrs.ObjectInPartialExpandedModule(pem, n.Addr) + o := &nodeModuleVariableInPartialModule{ + Addr: addr, + Config: n.Config, + Expr: n.Expr, + ModuleInstance: pem, + DestroyApply: n.DestroyApply, + } + g.Add(o) + }, + ) addRootNodeToGraph(&g) if checkableAddrs != nil { @@ -287,3 +301,26 @@ func (n *nodeModuleVariable) evalModuleVariable(ctx EvalContext, validateOnly bo return finalVal, diags.ErrWithWarnings() } + +// nodeModuleVariableInPartialModule represents an infinite set of possible +// input variable instances beneath a partially-expanded module instance prefix. +// +// Its job is to find a suitable placeholder value that approximates the +// values of all of those possible instances. Ideally that's a concrete +// known value if all instances would have the same value, an unknown value +// of a specific type if the definition produces a known type, or a +// totally-unknown value of unknown type in the worst case. +type nodeModuleVariableInPartialModule struct { + Addr addrs.InPartialExpandedModule[addrs.InputVariable] + Config *configs.Variable // Config is the var in the config + Expr hcl.Expression // Expr is the value expression given in the call + // ModuleInstance in order to create the appropriate context for evaluating + // ModuleCallArguments, ex. so count.index and each.key can resolve + ModuleInstance addrs.PartialExpandedModule + + // DestroyApply must be set to true when applying a destroy operation and + // false otherwise. + DestroyApply bool +} + +// TODO: Implement nodeModuleVariableInPartialModule.Execute diff --git a/internal/terraform/node_output.go b/internal/terraform/node_output.go index bc5b7912b11e..bfa53d1fa6cb 100644 --- a/internal/terraform/node_output.go +++ b/internal/terraform/node_output.go @@ -79,50 +79,63 @@ func (n *nodeExpandOutput) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagn } var g Graph - for _, module := range expander.ExpandModule(n.Module) { - absAddr := n.Addr.Absolute(module) - if checkableAddrs != nil { - checkableAddrs.Add(absAddr) - } - - // Find any recorded change for this output - var change *plans.OutputChangeSrc - var outputChanges []*plans.OutputChangeSrc - if module.IsRoot() { - outputChanges = changes.GetRootOutputChanges() - } else { - parent, call := module.Call() - outputChanges = changes.GetOutputChanges(parent, call) - } - for _, c := range outputChanges { - if c.Addr.String() == absAddr.String() { - change = c - break + forEachModuleInstance( + expander, n.Module, + func(module addrs.ModuleInstance) { + absAddr := n.Addr.Absolute(module) + if checkableAddrs != nil { + checkableAddrs.Add(absAddr) } - } - var node dag.Vertex - switch { - case module.IsRoot() && n.Destroying: - node = &NodeDestroyableOutput{ - Addr: absAddr, - Planning: n.Planning, + // Find any recorded change for this output + var change *plans.OutputChangeSrc + var outputChanges []*plans.OutputChangeSrc + if module.IsRoot() { + outputChanges = changes.GetRootOutputChanges() + } else { + parent, call := module.Call() + outputChanges = changes.GetOutputChanges(parent, call) + } + for _, c := range outputChanges { + if c.Addr.String() == absAddr.String() { + change = c + break + } } - default: - node = &NodeApplyableOutput{ - Addr: absAddr, - Config: n.Config, - Change: change, - RefreshOnly: n.RefreshOnly, - DestroyApply: n.Destroying, - Planning: n.Planning, + var node dag.Vertex + switch { + case module.IsRoot() && n.Destroying: + node = &NodeDestroyableOutput{ + Addr: absAddr, + Planning: n.Planning, + } + + default: + node = &NodeApplyableOutput{ + Addr: absAddr, + Config: n.Config, + Change: change, + RefreshOnly: n.RefreshOnly, + DestroyApply: n.Destroying, + Planning: n.Planning, + } } - } - log.Printf("[TRACE] Expanding output: adding %s as %T", absAddr.String(), node) - g.Add(node) - } + log.Printf("[TRACE] Expanding output: adding %s as %T", absAddr.String(), node) + g.Add(node) + }, + func(pem addrs.PartialExpandedModule) { + absAddr := addrs.ObjectInPartialExpandedModule(pem, n.Addr) + node := &nodeOutputInPartialModule{ + Addr: absAddr, + Config: n.Config, + RefreshOnly: n.RefreshOnly, + } + log.Printf("[TRACE] Expanding output: adding placeholder for all %s as %T", absAddr.String(), node) + g.Add(node) + }, + ) addRootNodeToGraph(&g) if checkableAddrs != nil { @@ -434,6 +447,25 @@ func (n *NodeApplyableOutput) DotNode(name string, opts *dag.DotOpts) *dag.DotNo } } +// nodeOutputInPartialModule represents an infinite set of possible output value +// instances beneath a partially-expanded module instance prefix. +// +// Its job is to find a suitable placeholder value that approximates the +// values of all of those possible instances. Ideally that's a concrete +// known value if all instances would have the same value, an unknown value +// of a specific type if the definition produces a known type, or a +// totally-unknown value of unknown type in the worst case. +type nodeOutputInPartialModule struct { + Addr addrs.InPartialExpandedModule[addrs.OutputValue] + Config *configs.Output + + // Refresh-only mode means that any failing output preconditions are + // reported as warnings rather than errors + RefreshOnly bool +} + +// TODO: Implement nodeOutputInPartialModule.Execute + // NodeDestroyableOutput represents an output that is "destroyable": // its application will remove the output from the state. type NodeDestroyableOutput struct { diff --git a/internal/terraform/node_resource_abstract.go b/internal/terraform/node_resource_abstract.go index cbf4f1d16e40..9af4acb5d56e 100644 --- a/internal/terraform/node_resource_abstract.go +++ b/internal/terraform/node_resource_abstract.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" @@ -402,19 +403,33 @@ func (n *NodeAbstractResource) writeResourceState(ctx EvalContext, addr addrs.Ab // to expand the module here to create all resources. expander := ctx.InstanceExpander() + // Allowing unknown values in count and for_each is currently only an + // experimental feature. This will hopefully become the default (and only) + // behavior in future, if the experiment is successful. + // + // If this is false then the codepaths that handle unknown values below + // become unreachable, because the evaluate functions will reject unknown + // values as an error. + allowUnknown := ctx.LanguageExperimentActive(experiments.UnknownInstances) + switch { case n.Config != nil && n.Config.Count != nil: - count, countDiags := evaluateCountExpression(n.Config.Count, ctx) + count, countDiags := evaluateCountExpression(n.Config.Count, ctx, allowUnknown) diags = diags.Append(countDiags) if countDiags.HasErrors() { return diags } state.SetResourceProvider(addr, n.ResolvedProvider) - expander.SetResourceCount(addr.Module, n.Addr.Resource, count) + if count >= 0 { + expander.SetResourceCount(addr.Module, n.Addr.Resource, count) + } else { + // -1 represents "unknown" + expander.SetResourceCountUnknown(addr.Module, n.Addr.Resource) + } case n.Config != nil && n.Config.ForEach != nil: - forEach, forEachDiags := evaluateForEachExpression(n.Config.ForEach, ctx) + forEach, known, forEachDiags := evaluateForEachExpression(n.Config.ForEach, ctx, allowUnknown) diags = diags.Append(forEachDiags) if forEachDiags.HasErrors() { return diags @@ -423,7 +438,11 @@ func (n *NodeAbstractResource) writeResourceState(ctx EvalContext, addr addrs.Ab // This method takes care of all of the business logic of updating this // while ensuring that any existing instances are preserved, etc. state.SetResourceProvider(addr, n.ResolvedProvider) - expander.SetResourceForEach(addr.Module, n.Addr.Resource, forEach) + if known { + expander.SetResourceForEach(addr.Module, n.Addr.Resource, forEach) + } else { + expander.SetResourceForEachUnknown(addr.Module, n.Addr.Resource) + } default: state.SetResourceProvider(addr, n.ResolvedProvider) diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index 960da8599e69..6db09484331a 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -772,7 +772,7 @@ func (n *NodeAbstractResourceInstance) plan( } // Evaluate the configuration - forEach, _ := evaluateForEachExpression(n.Config.ForEach, ctx) + forEach, _, _ := evaluateForEachExpression(n.Config.ForEach, ctx, false) keyData = EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach) @@ -1700,7 +1700,7 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule objTy := schema.ImpliedType() priorVal := cty.NullVal(objTy) - forEach, _ := evaluateForEachExpression(config.ForEach, ctx) + forEach, _, _ := evaluateForEachExpression(config.ForEach, ctx, false) keyData = EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach) checkDiags := evalCheckRules( @@ -1980,7 +1980,7 @@ func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned return nil, keyData, diags } - forEach, _ := evaluateForEachExpression(config.ForEach, ctx) + forEach, _, _ := evaluateForEachExpression(config.ForEach, ctx, false) keyData = EvalDataForInstanceKey(n.Addr.Resource.Key, forEach) checkDiags := evalCheckRules( @@ -2284,7 +2284,7 @@ func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state func (n *NodeAbstractResourceInstance) evalProvisionerConfig(ctx EvalContext, body hcl.Body, self cty.Value, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - forEach, forEachDiags := evaluateForEachExpression(n.Config.ForEach, ctx) + forEach, _, forEachDiags := evaluateForEachExpression(n.Config.ForEach, ctx, false) diags = diags.Append(forEachDiags) keyData := EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach) diff --git a/internal/terraform/node_resource_plan.go b/internal/terraform/node_resource_plan.go index 041d414293bd..8825160c41d0 100644 --- a/internal/terraform/node_resource_plan.go +++ b/internal/terraform/node_resource_plan.go @@ -218,7 +218,7 @@ func (n *nodeExpandPlannableResource) validateExpandedImportTargets() tfdiags.Di // // It has several side-effects: // - Adds a node to Graph g for each leaf resource instance it discovers, whether present or orphaned. -// - Registers the expansion of the resource in the "expander" object embedded inside EvalContext ctx. +// - Registers the expansion of the resource in the "expander" object embedded inside EvalContext globalCtx. // - Adds each present (non-orphaned) resource instance address to checkableAddrs (guaranteed to always be addrs.AbsResourceInstance, despite being declared as addrs.Checkable). // // After calling this for each of the module instances the resource appears @@ -378,7 +378,7 @@ func (n nodeExpandPlannableResource) expandResourceImports(ctx EvalContext, addr continue } - forEachData, forEachDiags := newForEachEvaluator(imp.Config.ForEach, ctx).ImportValues() + forEachData, forEachDiags := newForEachEvaluator(imp.Config.ForEach, ctx, false).ImportValues() diags = diags.Append(forEachDiags) if forEachDiags.HasErrors() { return imports, diags diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index 6322097d5ae4..d00b9f9bb998 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -355,7 +355,7 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext) // values, which could result in a post-condition check relying on that // value being inaccurate. Unless we decide to store the value of the // for-each expression in state, this is unavoidable. - forEach, _ := evaluateForEachExpression(n.Config.ForEach, ctx) + forEach, _, _ := evaluateForEachExpression(n.Config.ForEach, ctx, false) repeatData := EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach) checkDiags := evalCheckRules( @@ -463,7 +463,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. return nil, diags } - forEach, _ := evaluateForEachExpression(n.Config.ForEach, ctx) + forEach, _, _ := evaluateForEachExpression(n.Config.ForEach, ctx, false) keyData := EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach) configVal, _, configDiags := ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData) if configDiags.HasErrors() { diff --git a/internal/terraform/node_resource_validate.go b/internal/terraform/node_resource_validate.go index 253bcb0097b6..9d6df7094578 100644 --- a/internal/terraform/node_resource_validate.go +++ b/internal/terraform/node_resource_validate.go @@ -306,7 +306,7 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag } // Evaluate the for_each expression here so we can expose the diagnostics - forEachDiags := newForEachEvaluator(n.Config.ForEach, ctx).ValidateResourceValue() + forEachDiags := newForEachEvaluator(n.Config.ForEach, ctx, false).ValidateResourceValue() diags = diags.Append(forEachDiags) }