Skip to content

Commit

Permalink
terraform: EvalContextBuiltin supports partial-expanded module instances
Browse files Browse the repository at this point in the history
Previously an EvalContextBuiltin could be associated either with no module
at all or with a fully-qualified module instance, depending on whether
each graph node implements the interface required to announce association
with a particular module instance.

For supporting partial-expanded module prefixes we now need a third mode
where the context is associated with an addrs.PartialExpandedModule
address, which conceptually represents an unbounded set of possible
module instances that share a common known address prefix.

The new type evalContextScope serves as a sum type which represents these
three different situations. A nil value of that interface type represents
no scope at all, whereas the two implementations of that type represent
the two different possible scope types. Both scope types can produce
an addrs.Module that they represent, allowing some degree of shared code
between the two cases as long as no full instance addresses are required.

The most important part of this change is that method EvaluationScope now
uses a different implementation of lang.Data when the context scope is
a partial-expanded module address. That implementation only returns
placeholders that approximate what we expect all instances of the affected
modules to have in common, in the hope of still catching some downstream
errors even though full evaluation is being deferred to a future run.

We currently have no means for a graph node to announce itself as belonging
to a partially-expanded module address, and so in practice this new mode
is not yet reachable. Future commits will teach the graph walker to be
able to recognize and handle graph nodes that need this new treatment,
making their DynamicExpand/Execute methods use this new scope type.
  • Loading branch information
apparentlymart committed Jan 29, 2024
1 parent 39a6f3a commit 641837d
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 39 deletions.
4 changes: 4 additions & 0 deletions internal/terraform/eval_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
104 changes: 65 additions & 39 deletions internal/terraform/eval_context_builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
8 changes: 8 additions & 0 deletions internal/terraform/eval_context_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions internal/terraform/eval_context_scope.go
Original file line number Diff line number Diff line change
@@ -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()
}

0 comments on commit 641837d

Please sign in to comment.