Files
opentf/internal/lang/eval/module_instance.go
Martin Atkins 72a32f726d lang/eval: Beginnings of a different way to handle config eval
In "package tofu" today we try to do everything using a generic acyclic
graph model and generic graph walk, which _works_ but tends to make every
other part of the problem very hard to follow because we rely a lot on
sidecar shared mutable data structures to propagate results between the
isolated operations.

This is the beginning of an experimental new way to do it where the "graph"
is implied by a model that more closely represents how the language itself
works, with explicit modelling of the relationships between different
types of objects and letting results flow directly from one object to
another without any big shared mutable state.

There's still a lot to do before this is actually complete enough to
evaluate whether it's a viable new design, but I'm considering this a good
starting checkpoint since there's enough here to run a simple test of
propagating data all the way from input variables to output values via
intermediate local values.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-10-27 10:15:41 -07:00

221 lines
9.3 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package eval
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/lang/eval/internal/configgraph"
"github.com/opentofu/opentofu/internal/lang/exprs"
"github.com/opentofu/opentofu/internal/tfdiags"
)
type moduleInstanceCall struct {
// declRange is the source location of the header of the module block
// that is making this call, or some similar config construct that's
// acting like a module call.
//
// This should be nil for calls that are caused by something other than
// configuration, such as a top-level call to a root module caused by
// running an OpenTofu CLI command.
declRange *tfdiags.SourceRange
// inputValues describes how to build the values for the input variables
// for this instance of the module.
//
// For a call caused by a "module" block in a parent module, these would
// be closures binding the expressions written in the module block to
// the scope of the module block. The scope of the module block should
// include the each.key/each.value/count.index symbols initialized as
// appropriate for this specific instance of the module call. It's
// the caller of [compileModuleInstance]'s responsibility to set these
// up correctly so that the child module can be compiled with no direct
// awareness of where it's being called from.
inputValues map[addrs.InputVariable]exprs.Valuer
// TODO: provider instances from the "providers" argument in the
// calling module, once we have enough of this implemented for that
// to be useful. Will need to straighten out the address types
// for provider configs and instances in package addrs first so
// that we finally have a proper address type for a provider
// instance with an instance key.
}
// compileModuleInstance is the main entry point for binding a module
// configuration to information from an instance of a module call and
// producing a [configgraph.ModuleInstance] representation of the
// resulting module instance, ready for continued evaluation.
//
// For those coming to this familiar with the previous language runtime
// implementation in "package tofu": this is _roughly_ analogous to the
// graph building process but is focused only on the configuration of
// a single module (no state, no other modules) and is written as much
// as possible as straightforward linear code, with inversion of control
// techniques only where it's useful to separate concerns.
func compileModuleInstance(
module *configs.Module,
// FIXME: This is a separate argument for now because in current
// "package configs" this is treated as a property of the [configs.Config]
// instead of the [configs.Module], but we're intentionally not using
// [configs.Config] here because this new design assembles the module tree
// gradually during evaluation rather than up front during loading.
//
// If we decide to take this direction we should track the source
// address as a field of [configs.Module] so that we don't need this
// extra argument.
moduleSourceAddr addrs.ModuleSource,
call *moduleInstanceCall,
evalCtx *EvalContext,
) *configgraph.ModuleInstance {
// -----------------------------------------------------------------------
// This intentionally has no direct error return path, because:
// 1. The code that builds *configs.Module should already have reported
// any "static" problems like syntax errors and hard structural
// problems and thus prevented us from even reaching this call if
// any were present.
// 2. This "compiling" step is mainly about wiring things together in
// preparation for evaluation rather than actually evaluating, and so
// _dynamic_ problems will be handled during the subsequent evaluation
// step rather than during this compilation process.
//
// If the work performed by this function _does_ discover something that's
// invalid enough that it's impossible to construct valid evaluation
// objects, then use mechanisms like [exprs.ForceErrorValuer] to arrange
// for predefined error diagnostics to be discovered during evaluation
// instead of returning them directly from here.
// -----------------------------------------------------------------------
// We'll build this object up gradually because what we're ultimately going
// to return is an implied graph of the relationships between everything
// declared in this module, represented either directly by pointers or
// indirectly through expressions, and so for the remainder of this
// function we need to be careful in how we interact with the methods of
// [configgraph.ModuleInstance] since many of them only make sense to
// call everything has been completely assembled.
ret := &configgraph.ModuleInstance{
ModuleSourceAddr: moduleSourceAddr,
CallDeclRange: call.declRange,
}
ret.InputVariableNodes = compileModuleInstanceInputVariables(module.Variables, call.inputValues, ret, call.declRange)
ret.LocalValueNodes = compileModuleInstanceLocalValues(module.Locals, ret)
ret.OutputValueNodes = compileModuleInstanceOutputValues(module.Outputs, ret)
return ret
}
func compileModuleInstanceInputVariables(configs map[string]*configs.Variable, values map[addrs.InputVariable]exprs.Valuer, declScope exprs.Scope, missingDefRange *tfdiags.SourceRange) map[addrs.InputVariable]*configgraph.InputVariable {
ret := make(map[addrs.InputVariable]*configgraph.InputVariable, len(configs))
for name, vc := range configs {
addr := addrs.InputVariable{Name: name}
rawValue, ok := values[addr]
if !ok {
diagRange := vc.DeclRange
if missingDefRange != nil {
// better to blame the definition site than the declaration
// site if we have enough information to do that.
diagRange = missingDefRange.ToHCL()
}
if vc.Required() {
// We don't actually _need_ to handle an error here because
// the final evaluation of the variables must deal with the
// possibility of the final value being null anyway, but
// by handling this here we can produce a more helpful error
// message that talks about the definition being statically
// absent instead of dynamically null.
var diags tfdiags.Diagnostics
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing definition for required input variable",
Detail: fmt.Sprintf("Input variable %q is required, and so it must be provided as an argument to this module.", name),
Subject: &diagRange,
})
rawValue = exprs.ForcedErrorValuer(diags)
} else {
// For a non-required variable we'll provide a placeholder
// null value so that the evaluator can treat this the same
// as if there was an explicit definition evaluating to null.
rawValue = exprs.ConstantValuerWithSourceRange(
cty.NullVal(vc.Type),
tfdiags.SourceRangeFromHCL(diagRange),
)
}
}
ret[addr] = &configgraph.InputVariable{
DeclName: name,
RawValue: configgraph.OnceValuer(rawValue),
TargetType: vc.ConstraintType,
TargetDefaults: vc.TypeDefaults,
ValidationRules: compileCheckRules(vc.Validations, declScope, vc.Ephemeral),
}
}
return ret
}
func compileModuleInstanceLocalValues(configs map[string]*configs.Local, declScope exprs.Scope) map[addrs.LocalValue]*configgraph.LocalValue {
ret := make(map[addrs.LocalValue]*configgraph.LocalValue, len(configs))
for name, vc := range configs {
addr := addrs.LocalValue{Name: name}
value := configgraph.OnceValuer(exprs.NewClosure(
exprs.EvalableHCLExpression(vc.Expr),
declScope,
))
ret[addr] = &configgraph.LocalValue{
RawValue: value,
}
}
return ret
}
func compileModuleInstanceOutputValues(configs map[string]*configs.Output, declScope *configgraph.ModuleInstance) map[addrs.OutputValue]*configgraph.OutputValue {
ret := make(map[addrs.OutputValue]*configgraph.OutputValue, len(configs))
for name, vc := range configs {
addr := addrs.OutputValue{Name: name}
value := configgraph.OnceValuer(exprs.NewClosure(
exprs.EvalableHCLExpression(vc.Expr),
declScope,
))
ret[addr] = &configgraph.OutputValue{
DeclName: name,
RawValue: value,
// Our current language doesn't allow specifying a type constraint
// for an output value, so these are always the most liberal
// possible constraint. Making these customizable could be part
// of a solution to:
// https://github.com/opentofu/opentofu/issues/2831
TargetType: cty.DynamicPseudoType,
TargetDefaults: nil,
ForceSensitive: vc.Sensitive,
ForceEphemeral: vc.Ephemeral,
Preconditions: compileCheckRules(vc.Preconditions, declScope, vc.Ephemeral),
}
}
return ret
}
func compileCheckRules(configs []*configs.CheckRule, declScope exprs.Scope, ephemeralAllowed bool) []configgraph.CheckRule {
ret := make([]configgraph.CheckRule, 0, len(configs))
for _, config := range configs {
ret = append(ret, configgraph.CheckRule{
Condition: exprs.EvalableHCLExpression(config.Condition),
ErrorMessageRaw: exprs.EvalableHCLExpression(config.ErrorMessage),
ParentScope: declScope,
EphemeralAllowed: ephemeralAllowed,
DeclSourceRange: tfdiags.SourceRangeFromHCL(config.DeclRange),
})
}
return ret
}