diff --git a/internal/terraform/eval_context.go b/internal/terraform/eval_context.go index 5ab46f79a4d2..47dbbf60d880 100644 --- a/internal/terraform/eval_context.go +++ b/internal/terraform/eval_context.go @@ -193,4 +193,8 @@ type EvalContext interface { // WithPath returns a copy of the context with the internal path set to the // path argument. WithPath(path addrs.ModuleInstance) EvalContext + + // WithPartialExpandedPath returns a copy of the context with the internal + // path set to the path argument. + WithPartialExpandedPath(path addrs.PartialExpandedModule) EvalContext } diff --git a/internal/terraform/eval_context_builtin.go b/internal/terraform/eval_context_builtin.go index 08d59384a425..ac696a514e86 100644 --- a/internal/terraform/eval_context_builtin.go +++ b/internal/terraform/eval_context_builtin.go @@ -34,19 +34,17 @@ import ( // BuiltinEvalContext is an EvalContext implementation that is used by // Terraform by default. type BuiltinEvalContext struct { + // scope is the scope (module instance or set of possible module instances) + // that this context is operating within. + // + // Note: this can be evalContextGlobal (i.e. nil) when visiting a graph + // node that doesn't belong to a particular module, in which case any + // method using it will panic. + scope evalContextScope + // StopContext is the context used to track whether we're complete StopContext context.Context - // PathValue is the Path that this context is operating within. - PathValue addrs.ModuleInstance - - // pathSet indicates that this context was explicitly created for a - // specific path, and can be safely used for evaluation. This lets us - // differentiate between PathValue being unset, and the zero value which is - // equivalent to RootModuleInstance. Path and Evaluation methods will - // panic if this is not set. - pathSet bool - // Evaluator is used for evaluating expressions within the scope of this // eval context. Evaluator *Evaluator @@ -90,8 +88,17 @@ var _ EvalContext = (*BuiltinEvalContext)(nil) func (ctx *BuiltinEvalContext) WithPath(path addrs.ModuleInstance) EvalContext { newCtx := *ctx - newCtx.pathSet = true - newCtx.PathValue = path + newCtx.scope = evalContextModuleInstance{ + Addr: path, + } + return &newCtx +} + +func (ctx *BuiltinEvalContext) WithPartialExpandedPath(path addrs.PartialExpandedModule) EvalContext { + newCtx := *ctx + newCtx.scope = evalContextPartialExpandedModule{ + Addr: path, + } return &newCtx } @@ -436,28 +443,45 @@ func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, r } func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope { - if !ctx.pathSet { - panic("context path not set") - } - data := &evaluationStateData{ - Evaluator: ctx.Evaluator, - ModulePath: ctx.PathValue, - InstanceKeyData: keyData, - Operation: ctx.Evaluator.Operation, + switch scope := ctx.scope.(type) { + case evalContextModuleInstance: + data := &evaluationStateData{ + Evaluator: ctx.Evaluator, + ModulePath: scope.Addr, + InstanceKeyData: keyData, + Operation: ctx.Evaluator.Operation, + } + evalScope := ctx.Evaluator.Scope(data, self, source, ctx.evaluationExternalFunctions()) + + // ctx.PathValue is the path of the module that contains whatever + // expression the caller will be trying to evaluate, so this will + // activate only the experiments from that particular module, to + // be consistent with how experiment checking in the "configs" + // package itself works. The nil check here is for robustness in + // incompletely-mocked testing situations; mc should never be nil in + // real situations. + if mc := ctx.Evaluator.Config.DescendentForInstance(scope.Addr); mc != nil { + evalScope.SetActiveExperiments(mc.Module.ActiveExperiments) + } + return evalScope + case evalContextPartialExpandedModule: + data := &evaluationPlaceholderData{ + Evaluator: ctx.Evaluator, + ModulePath: scope.Addr, + CountAvailable: keyData.CountIndex != cty.NilVal, + EachAvailable: keyData.EachKey != cty.NilVal, + Operation: ctx.Evaluator.Operation, + } + evalScope := ctx.Evaluator.Scope(data, self, source, ctx.evaluationExternalFunctions()) + if mc := ctx.Evaluator.Config.Descendent(scope.Addr.Module()); mc != nil { + evalScope.SetActiveExperiments(mc.Module.ActiveExperiments) + } + return evalScope + default: + // This method is valid only for module-scoped EvalContext objects. + panic("no evaluation scope available: not in module context") } - scope := ctx.Evaluator.Scope(data, self, source, ctx.evaluationExternalFunctions()) - // ctx.PathValue is the path of the module that contains whatever - // expression the caller will be trying to evaluate, so this will - // activate only the experiments from that particular module, to - // be consistent with how experiment checking in the "configs" - // package itself works. The nil check here is for robustness in - // incompletely-mocked testing situations; mc should never be nil in - // real situations. - if mc := ctx.Evaluator.Config.DescendentForInstance(ctx.PathValue); mc != nil { - scope.SetActiveExperiments(mc.Module.ActiveExperiments) - } - return scope } // evaluationExternalFunctions is a helper for method EvaluationScope which @@ -531,10 +555,10 @@ func (ctx *BuiltinEvalContext) functionProvider(addr addrs.Provider) (providers. } func (ctx *BuiltinEvalContext) Path() addrs.ModuleInstance { - if !ctx.pathSet { - panic("context path not set") + if scope, ok := ctx.scope.(evalContextModuleInstance); ok { + return scope.Addr } - return ctx.PathValue + panic("not evaluating in the scope of a fully-expanded module") } func (ctx *BuiltinEvalContext) LanguageExperimentActive(experiment experiments.Experiment) bool { @@ -543,12 +567,14 @@ func (ctx *BuiltinEvalContext) LanguageExperimentActive(experiment experiments.E // 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. + scope := ctx.scope + if scope == evalContextGlobal { + // If we're not associated with a specific module then there can't + // be any language experiments in play, because experiment activation + // is module-scoped. return false } - cfg := ctx.Evaluator.Config.DescendentForInstance(ctx.Path()) + cfg := ctx.Evaluator.Config.Descendent(scope.evalContextScopeModule()) if cfg == nil { return false } diff --git a/internal/terraform/eval_context_mock.go b/internal/terraform/eval_context_mock.go index 89f31b5dd9b9..d96b89fc7360 100644 --- a/internal/terraform/eval_context_mock.go +++ b/internal/terraform/eval_context_mock.go @@ -332,6 +332,14 @@ func (c *MockEvalContext) WithPath(path addrs.ModuleInstance) EvalContext { return &newC } +func (c *MockEvalContext) WithPartialExpandedPath(path addrs.PartialExpandedModule) EvalContext { + // This is not yet implemented as a mock, because we've not yet had any + // need for it. If we end up needing to test this behavior in isolation + // somewhere then we'll need to figure out how to fit it in here without + // upsetting too many existing tests that rely on the PathPath field. + panic("WithPartialExpandedPath not implemented for MockEvalContext") +} + func (c *MockEvalContext) Path() addrs.ModuleInstance { c.PathCalled = true return c.PathPath diff --git a/internal/terraform/eval_context_scope.go b/internal/terraform/eval_context_scope.go new file mode 100644 index 000000000000..084835db7c2c --- /dev/null +++ b/internal/terraform/eval_context_scope.go @@ -0,0 +1,61 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "github.com/hashicorp/terraform/internal/addrs" +) + +// evalContextScope represents the scope that an [EvalContext] (or rather, +// an [EvalContextBuiltin] is associated with. +// +// This is a closed interface representing a sum type, with three possible +// variants: +// +// - a nil value of this type represents a "global" evaluation context used +// for graph nodes that aren't considered to belong to any specific module +// instance. Some [EvalContext] methods are not appropriate for such a +// context, and so will panic on a global evaluation context. +// - [evalContextModuleInstance] is for an evaluation context used for +// graph nodes that implement [GraphNodeModuleInstance], meaning that +// they belong to a fully-expanded single module instance. +// - [evalContextPartialExpandedModule] is for an evaluation context used for +// graph nodes that implement [GraphNodeUnexpandedModule], meaning that +// they belong to an unbounded set of possible module instances sharing +// a common known prefix, in situations where a module call has an unknown +// value for its count or for_each argument. +type evalContextScope interface { + // evalContextScopeModule returns the static module address of whatever + // fully- or partially-expanded module instance address this scope is + // associated with. + // + // A "global" evaluation context is a nil [evalContextScope], and so + // this method will panic for that scope. + evalContextScopeModule() addrs.Module +} + +// evalContextGlobal is the nil [evalContextScope] used to represent an +// [EvalContext] that isn't associated with any module at all. +var evalContextGlobal evalContextScope + +// evalContextModuleInstance is an [evalContextScope] associated with a +// fully-expanded single module instance. +type evalContextModuleInstance struct { + Addr addrs.ModuleInstance +} + +func (s evalContextModuleInstance) evalContextScopeModule() addrs.Module { + return s.Addr.Module() +} + +// evalContextPartialExpandedModule is an [evalContextScope] associated with +// an unbounded set of possible module instances that share a common known +// address prefix. +type evalContextPartialExpandedModule struct { + Addr addrs.PartialExpandedModule +} + +func (s evalContextPartialExpandedModule) evalContextScopeModule() addrs.Module { + return s.Addr.Module() +}