mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-29 10:01:07 -05:00
lang/eval: Start of supporting module calls
This stops short of actually implementing the evaluation of a module call, but gets the compilation behavior in place and the usual boilerplate for handling multi-instance module calls. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
@@ -22,7 +22,7 @@ import (
|
||||
// context such as available providers and module packages.
|
||||
type ConfigInstance struct {
|
||||
rootModuleSource addrs.ModuleSource
|
||||
inputValues map[addrs.InputVariable]exprs.Valuer
|
||||
inputValues exprs.Valuer
|
||||
evalContext *evalglue.EvalContext
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ type ConfigCall struct {
|
||||
// InputValues describes how to obtain values for the input variables
|
||||
// declared in the root module.
|
||||
//
|
||||
// In typical use the InputValues map is assembled based on a combination
|
||||
// In typical use the InputValues object is assembled based on a combination
|
||||
// of ".tfvars" files, CLI arguments, and environment variables, but that's
|
||||
// the responsibility of the Tofu CLI layer and so this package is totally
|
||||
// unopinionated about how those are provided, so e.g. for .tftest.hcl "run"
|
||||
@@ -49,7 +49,7 @@ type ConfigCall struct {
|
||||
// In unit tests where the source of input variables is immaterial,
|
||||
// [InputValuesForTesting] might be useful to build values for this
|
||||
// field inline in the test code.
|
||||
InputValues map[addrs.InputVariable]exprs.Valuer
|
||||
InputValues exprs.Valuer
|
||||
|
||||
// EvalContext describes the context where the call is being made, dealing
|
||||
// with cross-cutting concerns like which providers are available and how
|
||||
|
||||
115
internal/lang/eval/internal/configgraph/module_call.go
Normal file
115
internal/lang/eval/internal/configgraph/module_call.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package configgraph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/apparentlymart/go-workgraph/workgraph"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/instances"
|
||||
"github.com/opentofu/opentofu/internal/lang/exprs"
|
||||
"github.com/opentofu/opentofu/internal/lang/grapheval"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
type ModuleCall struct {
|
||||
Addr addrs.AbsModuleCall
|
||||
DeclRange tfdiags.SourceRange
|
||||
|
||||
// InstanceSelector represents a rule for deciding which instances of
|
||||
// this resource have been declared.
|
||||
InstanceSelector InstanceSelector
|
||||
|
||||
// CompileCallInstance is a callback function provided by whatever
|
||||
// compiled this [ModuleCall] object that knows how to produce a compiled
|
||||
// [ModuleCallInstance] object once we know of the instance key and
|
||||
// associated repetition data for it.
|
||||
//
|
||||
// This indirection allows the caller to take into account the same
|
||||
// context it had available when it built this [ModuleCall] object, while
|
||||
// incorporating the new information about this specific instance.
|
||||
CompileCallInstance func(ctx context.Context, key addrs.InstanceKey, repData instances.RepetitionData) *ModuleCallInstance
|
||||
|
||||
// instancesResult tracks the process of deciding which instances are
|
||||
// currently declared for this provider config, and the result of that process.
|
||||
//
|
||||
// Only the decideInstances method accesses this directly. Use that
|
||||
// method to obtain the coalesced result for use elsewhere.
|
||||
instancesResult grapheval.Once[*compiledInstances[*ModuleCallInstance]]
|
||||
}
|
||||
|
||||
var _ exprs.Valuer = (*ModuleCall)(nil)
|
||||
|
||||
// Instances returns the instances that are selected for this module call in
|
||||
// its configuration, without evaluating their configuration objects yet.
|
||||
func (c *ModuleCall) Instances(ctx context.Context) map[addrs.InstanceKey]*ModuleCallInstance {
|
||||
// We ignore the diagnostics here because they will be returned by
|
||||
// the Value method instead.
|
||||
result, _ := c.decideInstances(ctx)
|
||||
return result.Instances
|
||||
}
|
||||
|
||||
func (c *ModuleCall) decideInstances(ctx context.Context) (*compiledInstances[*ModuleCallInstance], tfdiags.Diagnostics) {
|
||||
return c.instancesResult.Do(ctx, func(ctx context.Context) (*compiledInstances[*ModuleCallInstance], tfdiags.Diagnostics) {
|
||||
return compileInstances(ctx, c.InstanceSelector, c.CompileCallInstance)
|
||||
})
|
||||
}
|
||||
|
||||
// StaticCheckTraversal implements exprs.Valuer.
|
||||
func (c *ModuleCall) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics {
|
||||
return staticCheckTraversalForInstances(c.InstanceSelector, traversal)
|
||||
}
|
||||
|
||||
// Value implements exprs.Valuer.
|
||||
func (c *ModuleCall) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
|
||||
selection, diags := c.decideInstances(ctx)
|
||||
return valueForInstances(ctx, selection), diags
|
||||
}
|
||||
|
||||
// ValueSourceRange implements exprs.Valuer.
|
||||
func (c *ModuleCall) ValueSourceRange() *tfdiags.SourceRange {
|
||||
return &c.DeclRange
|
||||
}
|
||||
|
||||
// CheckAll implements allChecker.
|
||||
func (c *ModuleCall) CheckAll(ctx context.Context) tfdiags.Diagnostics {
|
||||
var cg CheckGroup
|
||||
// Our InstanceSelector itself might block on expression evaluation,
|
||||
// so we'll run it async as part of the checkGroup.
|
||||
cg.Await(ctx, func(ctx context.Context) {
|
||||
for _, inst := range c.Instances(ctx) {
|
||||
cg.CheckValuer(ctx, inst)
|
||||
}
|
||||
})
|
||||
// This is where an invalid for_each expression would be reported.
|
||||
cg.CheckValuer(ctx, c)
|
||||
return cg.Complete(ctx)
|
||||
}
|
||||
|
||||
func (c *ModuleCall) AnnounceAllGraphevalRequests(announce func(workgraph.RequestID, grapheval.RequestInfo)) {
|
||||
// There might be other grapheval requests in our dynamic instances, but
|
||||
// they are hidden behind another request themselves so we'll try to
|
||||
// report them only if that request was already started.
|
||||
instancesReqId := c.instancesResult.RequestID()
|
||||
if instancesReqId == workgraph.NoRequest {
|
||||
return
|
||||
}
|
||||
announce(instancesReqId, grapheval.RequestInfo{
|
||||
Name: fmt.Sprintf("decide instances for %s", c.Addr),
|
||||
SourceRange: c.InstanceSelector.InstancesSourceRange(),
|
||||
})
|
||||
// The Instances method potentially starts a new request, but we already
|
||||
// confirmed above that this request was already started and so we
|
||||
// can safely just await its result here.
|
||||
for _, inst := range c.Instances(grapheval.ContextWithNewWorker(context.Background())) {
|
||||
inst.AnnounceAllGraphevalRequests(announce)
|
||||
}
|
||||
}
|
||||
115
internal/lang/eval/internal/configgraph/module_call_instance.go
Normal file
115
internal/lang/eval/internal/configgraph/module_call_instance.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package configgraph
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/apparentlymart/go-workgraph/workgraph"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/lang/exprs"
|
||||
"github.com/opentofu/opentofu/internal/lang/grapheval"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
type ModuleCallInstance struct {
|
||||
// ModuleInstanceAddr is the address of the module instance that this
|
||||
// call instance is establishing.
|
||||
//
|
||||
// The difference between an instance of a module call and an instance
|
||||
// of a module is a little fussy and pedantic: the call instance is
|
||||
// viewed from the perspective of the caller while the module instance
|
||||
// is viewed from the perspective of the callee. But outside of package
|
||||
// configgraph that is not a distinction we make and so we don't have
|
||||
// a separate address type for an "absolute module call instance".
|
||||
ModuleInstanceAddr addrs.ModuleInstance
|
||||
|
||||
// SourceAddrValuer and VersionConstraintValuer together describe how
|
||||
// to select the module to be called.
|
||||
//
|
||||
// Allowing the entire module content to vary between phases is too
|
||||
// much chaos for our plan/apply model to really support, so we make
|
||||
// a pragmatic compromise here of disallowing the results from these
|
||||
// to be derived from any resource instances (even if the value happens
|
||||
// to be currently known) and just generally disallowing unknown
|
||||
// values regardless of where they are coming from. In practice resource
|
||||
// instances are the main place unknown values come from, but this
|
||||
// also excludes specifying the module to use based on impure functions
|
||||
// like "timestamp" whose results aren't decided until the apply step.
|
||||
//
|
||||
// These are associated with call instances rather than the main call,
|
||||
// and so it's possible for different instances of the same call to
|
||||
// select completely different modules. While that's a somewhat esoteric
|
||||
// thing to do, it would make it possible to e.g. write a module call that
|
||||
// uses for_each where the associated values choose between multiple
|
||||
// implementations of the same general abstraction. However, our surface
|
||||
// language doesn't currently allow that -- it always evaluates these
|
||||
// in the global scope rather than per-instance scope -- because the
|
||||
// way module blocks are currently designed means that HCL wants the
|
||||
// set of arguments to be fixed statically rather than chosen
|
||||
// dynamically.
|
||||
SourceAddrValuer *OnceValuer
|
||||
VersionConstraintValuer *OnceValuer
|
||||
|
||||
// InputsValuer is a valuer for all of the input variable values taken
|
||||
// together as a single object. It's structured this way mainly for
|
||||
// consistency with how we deal with the objects representing arguments
|
||||
// in other blocks, but it also means that a future edition of the
|
||||
// language could potentially use different syntax for input variables
|
||||
// that allows constructing the entire map dynamically using expression
|
||||
// syntax.
|
||||
InputsValuer *OnceValuer
|
||||
}
|
||||
|
||||
var _ exprs.Valuer = (*ModuleCallInstance)(nil)
|
||||
|
||||
// StaticCheckTraversal implements exprs.Valuer.
|
||||
func (m *ModuleCallInstance) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics {
|
||||
// We only do dynamic checks of accessing a module call because we
|
||||
// can't know what result type it will return without fetching and
|
||||
// compiling the child module source code, and that's too heavy
|
||||
// an operation for "static check".
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements exprs.Valuer.
|
||||
func (m *ModuleCallInstance) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
|
||||
// TODO: Evaluate the source address and version constraint and then
|
||||
// use a new field with a callback to ask the compile layer to compile
|
||||
// us a [evalglue.CompiledModuleInstance] for the child module, and
|
||||
// then ask for its result value and return it.
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// ValueSourceRange implements exprs.Valuer.
|
||||
func (m *ModuleCallInstance) ValueSourceRange() *tfdiags.SourceRange {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckAll implements allChecker.
|
||||
func (c *ModuleCallInstance) CheckAll(ctx context.Context) tfdiags.Diagnostics {
|
||||
var cg CheckGroup
|
||||
cg.CheckValuer(ctx, c)
|
||||
return cg.Complete(ctx)
|
||||
}
|
||||
|
||||
func (m *ModuleCallInstance) AnnounceAllGraphevalRequests(announce func(workgraph.RequestID, grapheval.RequestInfo)) {
|
||||
announce(m.SourceAddrValuer.RequestID(), grapheval.RequestInfo{
|
||||
Name: m.ModuleInstanceAddr.String() + " source address",
|
||||
SourceRange: m.SourceAddrValuer.ValueSourceRange(),
|
||||
})
|
||||
announce(m.VersionConstraintValuer.RequestID(), grapheval.RequestInfo{
|
||||
Name: m.ModuleInstanceAddr.String() + " version constraint",
|
||||
SourceRange: m.VersionConstraintValuer.ValueSourceRange(),
|
||||
})
|
||||
announce(m.InputsValuer.RequestID(), grapheval.RequestInfo{
|
||||
Name: m.ModuleInstanceAddr.String() + " input variable values",
|
||||
SourceRange: m.InputsValuer.ValueSourceRange(),
|
||||
})
|
||||
}
|
||||
@@ -104,6 +104,12 @@ func CompileModuleInstance(
|
||||
ret.inputVariableNodes = compileModuleInstanceInputVariables(ctx, module.Variables, call.InputValues, topScope, call.CalleeAddr, call.DeclRange)
|
||||
ret.localValueNodes = compileModuleInstanceLocalValues(ctx, module.Locals, topScope, call.CalleeAddr)
|
||||
ret.outputValueNodes = compileModuleInstanceOutputValues(ctx, module.Outputs, topScope, call.CalleeAddr)
|
||||
ret.moduleCallNodes = compileModuleInstanceModuleCalls(ctx,
|
||||
module.ModuleCalls,
|
||||
topScope,
|
||||
providersSidechannel,
|
||||
call.CalleeAddr,
|
||||
)
|
||||
ret.resourceNodes = compileModuleInstanceResources(ctx,
|
||||
module.ManagedResources,
|
||||
module.DataResources,
|
||||
@@ -118,47 +124,91 @@ func CompileModuleInstance(
|
||||
return ret
|
||||
}
|
||||
|
||||
func compileModuleInstanceInputVariables(_ context.Context, configs map[string]*configs.Variable, values map[addrs.InputVariable]exprs.Valuer, declScope exprs.Scope, moduleInstAddr addrs.ModuleInstance, missingDefRange *tfdiags.SourceRange) map[addrs.InputVariable]*configgraph.InputVariable {
|
||||
func compileModuleInstanceInputVariables(_ context.Context, configs map[string]*configs.Variable, values exprs.Valuer, declScope exprs.Scope, moduleInstAddr addrs.ModuleInstance, 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()
|
||||
// The valuer for an individual input variable derives from the
|
||||
// valuer for the single object representing all of the input
|
||||
// variables together.
|
||||
rawValuer := exprs.DerivedValuer(values, func(v cty.Value, _ tfdiags.Diagnostics) (cty.Value, tfdiags.Diagnostics) {
|
||||
// We intentionally avoid passing on the diagnostics from the
|
||||
// "values" valuer here both because they will be about the
|
||||
// entire object rather than the individual attribute we're
|
||||
// interested in and because whatever produced the "values"
|
||||
// valuer should've already reported its own errors when
|
||||
// it was checked directly.
|
||||
//
|
||||
// We might return additional diagnostics about the individual
|
||||
// atribute we're extracting, though.
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
defRange := missingDefRange
|
||||
if valueRange := values.ValueSourceRange(); valueRange != nil {
|
||||
defRange = valueRange
|
||||
}
|
||||
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
|
||||
|
||||
ty := v.Type()
|
||||
if ty == cty.DynamicPseudoType {
|
||||
return cty.DynamicVal.WithSameMarks(v), diags
|
||||
}
|
||||
if !ty.IsObjectType() {
|
||||
// Should not get here because the caller should always pass
|
||||
// us an object type based on the arguments in the module
|
||||
// call, but we'll deal with it anyway for robustness.
|
||||
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,
|
||||
Summary: "Invalid input values",
|
||||
Detail: fmt.Sprintf("Input variable values for %s module must be provided as an object value, not %s.", moduleInstAddr, ty.FriendlyName()),
|
||||
Subject: configgraph.MaybeHCLSourceRange(defRange),
|
||||
})
|
||||
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),
|
||||
)
|
||||
return cty.DynamicVal.WithSameMarks(v), diags
|
||||
}
|
||||
}
|
||||
if v.IsNull() {
|
||||
// Again this suggests a bug in the caller, but we'll handle
|
||||
// it for robustness.
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid input values",
|
||||
Detail: fmt.Sprintf("The object describing the input values for %s must not be null.", moduleInstAddr),
|
||||
Subject: configgraph.MaybeHCLSourceRange(defRange),
|
||||
})
|
||||
return cty.DynamicVal.WithSameMarks(v), diags
|
||||
}
|
||||
|
||||
if !ty.HasAttribute(name) {
|
||||
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: configgraph.MaybeHCLSourceRange(defRange),
|
||||
})
|
||||
return cty.DynamicVal.WithSameMarks(v), 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.
|
||||
return cty.NullVal(cty.DynamicPseudoType).WithSameMarks(v), diags
|
||||
}
|
||||
}
|
||||
// After all of the checks above we should now be able to call
|
||||
// GetAttr for this name without panicking. (If v is unknown
|
||||
// or marked then cty will automatically return a derived unknown
|
||||
// or marked value.)
|
||||
return v.GetAttr(name), diags
|
||||
})
|
||||
ret[addr] = &configgraph.InputVariable{
|
||||
Addr: moduleInstAddr.InputVariable(name),
|
||||
RawValue: configgraph.ValuerOnce(rawValue),
|
||||
RawValue: configgraph.ValuerOnce(rawValuer),
|
||||
TargetType: vc.ConstraintType,
|
||||
TargetDefaults: vc.TypeDefaults,
|
||||
CompileValidationRules: func(ctx context.Context, value cty.Value) iter.Seq[*configgraph.CheckRule] {
|
||||
@@ -324,6 +374,73 @@ func compileModuleInstanceResource(
|
||||
return resourceAddr, ret
|
||||
}
|
||||
|
||||
func compileModuleInstanceModuleCalls(
|
||||
ctx context.Context,
|
||||
configs map[string]*configs.ModuleCall,
|
||||
declScope exprs.Scope,
|
||||
providersSidechannel *moduleProvidersSideChannel,
|
||||
moduleInstanceAddr addrs.ModuleInstance,
|
||||
) map[addrs.ModuleCall]*configgraph.ModuleCall {
|
||||
ret := make(map[addrs.ModuleCall]*configgraph.ModuleCall, len(configs))
|
||||
for name, config := range configs {
|
||||
addr := addrs.ModuleCall{Name: name}
|
||||
ret[addr] = &configgraph.ModuleCall{
|
||||
Addr: addr.Absolute(moduleInstanceAddr),
|
||||
DeclRange: tfdiags.SourceRangeFromHCL(config.DeclRange),
|
||||
InstanceSelector: compileInstanceSelector(ctx, declScope, config.ForEach, config.Count, nil),
|
||||
CompileCallInstance: func(ctx context.Context, key addrs.InstanceKey, repData instances.RepetitionData) *configgraph.ModuleCallInstance {
|
||||
var versionConstraintValuer exprs.Valuer
|
||||
if config.VersionAttr != nil {
|
||||
versionConstraintValuer = exprs.NewClosure(
|
||||
exprs.EvalableHCLExpression(config.VersionAttr.Expr),
|
||||
declScope,
|
||||
)
|
||||
} else {
|
||||
versionConstraintValuer = exprs.ConstantValuer(cty.NullVal(cty.String))
|
||||
}
|
||||
|
||||
instanceScope := instanceLocalScope(declScope, repData)
|
||||
return &configgraph.ModuleCallInstance{
|
||||
ModuleInstanceAddr: addr.Absolute(moduleInstanceAddr).Instance(key),
|
||||
|
||||
// We _could_ potentially allow the source address and
|
||||
// version constraint to vary between instances by
|
||||
// binding these to the instance local scope, but we
|
||||
// choose not to for now because the syntax for module
|
||||
// blocks means it's not possible to vary which input
|
||||
// variables are defined on a per-instance basis and so
|
||||
// selecting different modules wouldn't work well unless
|
||||
// they all had exactly the same input variable names.
|
||||
SourceAddrValuer: configgraph.ValuerOnce(exprs.NewClosure(
|
||||
exprs.EvalableHCLExpression(config.Source),
|
||||
declScope,
|
||||
)),
|
||||
VersionConstraintValuer: configgraph.ValuerOnce(
|
||||
versionConstraintValuer,
|
||||
),
|
||||
|
||||
// The inputs value _can_ be derived from per-instance
|
||||
// values though, of course! We use "just attributes"
|
||||
// mode here because on the caller side we don't yet know
|
||||
// what input variables the callee is expecting. We'll
|
||||
// just send this whole value over to it and let it
|
||||
// check whether the object type is acceptable.
|
||||
InputsValuer: configgraph.ValuerOnce(exprs.NewClosure(
|
||||
exprs.EvalableHCLBodyJustAttributes(config.Config),
|
||||
instanceScope,
|
||||
)),
|
||||
|
||||
// TODO: valuers for the "providers side-channel" from
|
||||
// the "providers" meta-argument, or automatic passing
|
||||
// of all of the default (unaliased) providers from
|
||||
// the parent module if "providers" isn't present.
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func compileModuleInstanceProviderConfigs(
|
||||
ctx context.Context,
|
||||
configs map[string]*configs.Provider,
|
||||
|
||||
@@ -31,6 +31,7 @@ type CompiledModuleInstance struct {
|
||||
localValueNodes map[addrs.LocalValue]*configgraph.LocalValue
|
||||
outputValueNodes map[addrs.OutputValue]*configgraph.OutputValue
|
||||
resourceNodes map[addrs.Resource]*configgraph.Resource
|
||||
moduleCallNodes map[addrs.ModuleCall]*configgraph.ModuleCall
|
||||
providerConfigNodes map[addrs.LocalProviderConfig]*configgraph.ProviderConfig
|
||||
}
|
||||
|
||||
@@ -52,6 +53,9 @@ func (c *CompiledModuleInstance) CheckAll(ctx context.Context) tfdiags.Diagnosti
|
||||
for _, n := range c.resourceNodes {
|
||||
cg.CheckChild(ctx, n)
|
||||
}
|
||||
for _, n := range c.moduleCallNodes {
|
||||
cg.CheckChild(ctx, n)
|
||||
}
|
||||
for _, n := range c.providerConfigNodes {
|
||||
cg.CheckChild(ctx, n)
|
||||
}
|
||||
@@ -105,6 +109,9 @@ func (c *CompiledModuleInstance) AnnounceAllGraphevalRequests(announce func(work
|
||||
for _, n := range c.resourceNodes {
|
||||
n.AnnounceAllGraphevalRequests(announce)
|
||||
}
|
||||
for _, n := range c.moduleCallNodes {
|
||||
n.AnnounceAllGraphevalRequests(announce)
|
||||
}
|
||||
for _, n := range c.providerConfigNodes {
|
||||
n.AnnounceAllGraphevalRequests(announce)
|
||||
}
|
||||
|
||||
@@ -30,15 +30,11 @@ type ModuleInstanceCall struct {
|
||||
// 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
|
||||
// This is a single valuer because when constructing this the caller
|
||||
// doesn't yet know what input variables the child module is expecting,
|
||||
// and so it just sends over whatever was present in the call and expects
|
||||
// the callee to reject it if it isn't compatible with the callee's API.
|
||||
InputValues exprs.Valuer
|
||||
|
||||
// ProvidersFromParent are values representing provider instances passed in
|
||||
// through our side-channel using the "providers" meta argument in the
|
||||
|
||||
@@ -8,17 +8,12 @@ package eval
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/lang/exprs"
|
||||
)
|
||||
|
||||
// InputValuesForTesting returns input variable definitions based on a constant
|
||||
// map, intended for convenient test setup in unit tests where it only matters
|
||||
// what the variable values are and not how they are provided.
|
||||
func InputValuesForTesting(vals map[string]cty.Value) map[addrs.InputVariable]exprs.Valuer {
|
||||
ret := make(map[addrs.InputVariable]exprs.Valuer, len(vals))
|
||||
for name, val := range vals {
|
||||
ret[addrs.InputVariable{Name: name}] = exprs.ConstantValuer(val)
|
||||
}
|
||||
return ret
|
||||
func InputValuesForTesting(vals map[string]cty.Value) exprs.Valuer {
|
||||
return exprs.ConstantValuer(cty.ObjectVal(vals))
|
||||
}
|
||||
|
||||
@@ -240,3 +240,94 @@ func (h *hclBody) EvalableSourceRange() tfdiags.SourceRange {
|
||||
// is _close_ to the body being described.
|
||||
return tfdiags.SourceRangeFromHCL(h.body.MissingItemRange())
|
||||
}
|
||||
|
||||
// hclBodyJustAttributes implements [Evalable] for a [hcl.Body] using HCL's
|
||||
// "just attributes" evaluation mode.
|
||||
type hclBodyJustAttributes struct {
|
||||
body hcl.Body
|
||||
}
|
||||
|
||||
// EvalableHCLBody returns an [Evalable] that evaluates the given HCL body
|
||||
// in HCL's "just attributes" mode, and then returns an object value whose
|
||||
// attribute names and values are derived from the result.
|
||||
func EvalableHCLBodyJustAttributes(body hcl.Body) Evalable {
|
||||
return &hclBodyJustAttributes{
|
||||
body: body,
|
||||
}
|
||||
}
|
||||
|
||||
// EvalableSourceRange implements Evalable.
|
||||
func (h *hclBodyJustAttributes) EvalableSourceRange() tfdiags.SourceRange {
|
||||
return tfdiags.SourceRangeFromHCL(h.body.MissingItemRange())
|
||||
}
|
||||
|
||||
// Evaluate implements Evalable.
|
||||
func (h *hclBodyJustAttributes) Evaluate(ctx context.Context, hclCtx *hcl.EvalContext) (cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
attrs, hclDiags := h.body.JustAttributes()
|
||||
diags = diags.Append(hclDiags)
|
||||
// If we have errors already then we might not have the full set of
|
||||
// attributes that were declared, so we'll return cty.DynamicVal below
|
||||
// to avoid overpromising that we know the result type. However,
|
||||
// we'll still visit all of the attributes we _did_ find in case
|
||||
// that allows us to collect up some more expression-errors to return so we
|
||||
// can tell the module author about as much as possible at once.
|
||||
typeKnown := true
|
||||
if hclDiags.HasErrors() {
|
||||
typeKnown = false
|
||||
}
|
||||
retAttrs := make(map[string]cty.Value, len(attrs))
|
||||
for name, attr := range attrs {
|
||||
val, hclDiags := attr.Expr.Value(hclCtx)
|
||||
diags = diags.Append(hclDiags)
|
||||
if hclDiags.HasErrors() {
|
||||
val = AsEvalError(cty.DynamicVal)
|
||||
}
|
||||
retAttrs[name] = val
|
||||
}
|
||||
if !typeKnown {
|
||||
return EvalResult(cty.DynamicVal, diags)
|
||||
}
|
||||
// We don't use a top-level EvalError here because we selectively
|
||||
// marked individual attribute values above, and we're confident
|
||||
// that the set of attribute names in this object is correct because
|
||||
// the original JustAttributes call succeeded.
|
||||
return cty.ObjectVal(retAttrs), diags
|
||||
}
|
||||
|
||||
// FunctionCalls implements Evalable.
|
||||
func (h *hclBodyJustAttributes) FunctionCalls() iter.Seq[*hcl.StaticCall] {
|
||||
// For now this is not implemented because the underlying HCL API
|
||||
// isn't the right shape to implement this method.
|
||||
return func(yield func(*hcl.StaticCall) bool) {}
|
||||
}
|
||||
|
||||
// References implements Evalable.
|
||||
func (h *hclBodyJustAttributes) References() iter.Seq[hcl.Traversal] {
|
||||
// This case is annoying because we need to perform the shallow
|
||||
// JustAttributes call to get the expressions to analyze, but then
|
||||
// we'll need to call it again in Evaluate once the scope has
|
||||
// been built. But only if we find this being a performance problem
|
||||
// should we consider trying to cache this result.
|
||||
//
|
||||
// We ignore the diagnostics here because we're just making a best
|
||||
// effort to learn what traversals we might need and then we'll
|
||||
// return the same set of diagnostics from Evaluate.
|
||||
attrs, _ := h.body.JustAttributes()
|
||||
return func(yield func(hcl.Traversal) bool) {
|
||||
for _, attr := range attrs {
|
||||
for _, traversal := range attr.Expr.Variables() {
|
||||
if !yield(traversal) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ResultTypeConstraint implements Evalable.
|
||||
func (h *hclBodyJustAttributes) ResultTypeConstraint() cty.Type {
|
||||
// We cannot predict a result type in "just attributes" mode because
|
||||
// the type depends on the results of the expressions in the body.
|
||||
return cty.DynamicPseudoType
|
||||
}
|
||||
|
||||
@@ -176,3 +176,34 @@ func (f forcedErrorValuer) References() iter.Seq[hcl.Traversal] {
|
||||
func (f forcedErrorValuer) ResultTypeConstraint() cty.Type {
|
||||
return cty.DynamicPseudoType
|
||||
}
|
||||
|
||||
// DerivedValuer returns a [Valuer] that first evaluates the source valuer
|
||||
// and then passes its results to the "project" function, before returning
|
||||
// whatever that returns.
|
||||
//
|
||||
// The source range of the returned valuer is the same as the source valuer.
|
||||
func DerivedValuer(source Valuer, project func(cty.Value, tfdiags.Diagnostics) (cty.Value, tfdiags.Diagnostics)) Valuer {
|
||||
return derivedValuer{source, project}
|
||||
}
|
||||
|
||||
type derivedValuer struct {
|
||||
source Valuer
|
||||
project func(cty.Value, tfdiags.Diagnostics) (cty.Value, tfdiags.Diagnostics)
|
||||
}
|
||||
|
||||
// StaticCheckTraversal implements Valuer.
|
||||
func (d derivedValuer) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics {
|
||||
// We can't predict what the "project" function will return, so we'll
|
||||
// just wait until dynamic eval time to check.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements Valuer.
|
||||
func (d derivedValuer) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
|
||||
return d.project(d.source.Value(ctx))
|
||||
}
|
||||
|
||||
// ValueSourceRange implements Valuer.
|
||||
func (d derivedValuer) ValueSourceRange() *tfdiags.SourceRange {
|
||||
return d.source.ValueSourceRange()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user