Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: Plumbing for graph nodes that belong to partially-expanded module prefixes #34571

Merged
merged 5 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
}
7 changes: 7 additions & 0 deletions internal/terraform/evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ var EvalDataForNoInstanceKey = InstanceKeyEvalData{}
// evaluationStateData must implement lang.Data
var _ lang.Data = (*evaluationStateData)(nil)

// StaticValidateReferences calls [Evaluator.StaticValidateReferences] on
// the evaluator embedded in this data object, using this data object's
// static module path.
func (d *evaluationStateData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics {
return d.Evaluator.StaticValidateReferences(refs, d.ModulePath.Module(), self, source)
}

func (d *evaluationStateData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
switch addr.Name {
Expand Down
Loading
Loading