lang/eval: Some initial support for child module calls

The way this all fits together doesn't feel quite right yet since there's
a bunch of wrapping layers that aren't really adding anything except
indirection between the packages, but this is at least a starting point
for child module support.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Martin Atkins
2025-09-08 14:54:47 -07:00
parent b06287111b
commit 64ad4cc66a
19 changed files with 1284 additions and 138 deletions

View File

@@ -113,6 +113,14 @@ func ParseModuleSource(raw string) (ModuleSource, error) {
return remoteAddr, nil
}
func MustParseModuleSource(raw string) ModuleSource {
ret, err := ParseModuleSource(raw)
if err != nil {
panic(err)
}
return ret
}
// ModuleSourceLocal is a ModuleSource representing a local path reference
// from the caller's directory to the callee's directory within the same
// module package.

View File

@@ -0,0 +1,72 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package addrs
import (
"fmt"
"io/fs"
"path"
)
// FIXME: Everything in here should have unit tests
// ResolveRelativeModuleSource calculates a new source address from the
// combination of two other source addresses, if possible.
func ResolveRelativeModuleSource(a, b ModuleSource) (ModuleSource, error) {
bLocal, ok := b.(ModuleSourceLocal)
if !ok {
return b, nil // non-local source addresses are always absolute
}
bRaw := string(bLocal)
switch a := a.(type) {
case ModuleSourceLocal:
aRaw := string(a)
new := path.Join(aRaw, bRaw)
if !isModuleSourceLocal(new) {
new = "./" + new // ModuleSourceLocal must always have a suitable prefix
}
return ModuleSourceLocal(new), nil
case ModuleSourceRegistry:
aSub := a.Subdir
newSub, err := joinModuleSourceSubPath(aSub, bRaw)
if err != nil {
return nil, fmt.Errorf("invalid relative path from %s: %w", a.String(), err)
}
return ModuleSourceRegistry{
Package: a.Package,
Subdir: newSub,
}, nil
case ModuleSourceRemote:
aSub := a.Subdir
newSub, err := joinModuleSourceSubPath(aSub, bRaw)
if err != nil {
return nil, fmt.Errorf("invalid relative path from %s: %w", a.String(), err)
}
return ModuleSourceRemote{
Package: a.Package,
Subdir: newSub,
}, nil
default:
// Should not get here, because the cases above should cover all
// of the implementations of [ModuleSource].
panic(fmt.Sprintf("unsupported ModuleSource type %T", a))
}
}
func joinModuleSourceSubPath(subPath, rel string) (string, error) {
new := path.Join(subPath, rel)
if new == "." {
return "", nil // the root of the package
}
// If subPath was valid then "." and ".." segments can only appear
// if "rel" has too many "../" segments, causing it to "escape" from
// its package root.
if !fs.ValidPath(new) {
return "", fmt.Errorf("relative path %s has too many \"../\" segments", rel)
}
return new, nil
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/lang/eval/internal/evalglue"
"github.com/opentofu/opentofu/internal/lang/eval/internal/tofu2024"
"github.com/opentofu/opentofu/internal/lang/exprs"
"github.com/opentofu/opentofu/internal/tfdiags"
)
@@ -108,13 +107,12 @@ func (c *ConfigInstance) newRootModuleInstance(ctx context.Context, glue evalglu
if diags.HasErrors() {
return nil, diags
}
rootModuleCall := &tofu2024.ModuleInstanceCall{
CalleeAddr: addrs.RootModuleInstance,
ret, moreDiags := rootModule.CompileModuleInstance(ctx, &evalglue.ModuleCall{
InputValues: c.inputValues,
EvaluationGlue: glue,
AllowImpureFunctions: c.allowImpureFunctions,
EvaluationGlue: glue,
EvalContext: c.evalContext,
}
rootModuleInstance := tofu2024.CompileModuleInstance(ctx, rootModule, c.rootModuleSource, rootModuleCall)
return rootModuleInstance, diags
})
diags = diags.Append(moreDiags)
return ret, diags
}

View File

@@ -220,3 +220,68 @@ func TestValidate_resourceValid(t *testing.T) {
t.Fatalf("unexpected errors: %s", diags.Err())
}
}
func TestValidate_childModuleCallValuesOnly(t *testing.T) {
configInst, diags := eval.NewConfigInstance(t.Context(), &eval.ConfigCall{
EvalContext: evalglue.EvalContextForTesting(t, &eval.EvalContext{
Modules: eval.ModulesForTesting(map[addrs.ModuleSourceLocal]*configs.Module{
addrs.ModuleSourceLocal("."): configs.ModuleFromStringForTesting(t, `
variable "in" {
type = string
}
module "child" {
source = "./child"
input = var.in
}
output "out" {
value = module.child.result
}
`),
addrs.ModuleSourceLocal("./child"): configs.ModuleFromStringForTesting(t, `
variable "input" {
type = string
}
output "result" {
value = var.input
}
`),
}),
Providers: eval.ProvidersForTesting(map[addrs.Provider]*providers.GetProviderSchemaResponse{
addrs.MustParseProviderSourceString("test/foo"): {
Provider: providers.Schema{
Block: &configschema.Block{},
},
ResourceTypes: map[string]providers.Schema{
"foo": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {
Type: cty.String,
Required: true,
},
"id": {
Type: cty.String,
Computed: true,
},
},
},
},
},
},
}),
}),
RootModuleSource: addrs.ModuleSourceLocal("."),
InputValues: eval.InputValuesForTesting(map[string]cty.Value{
"in": cty.StringVal("foo bar baz"),
}),
})
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
diags = configInst.Validate(t.Context())
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/lang/eval/internal/evalglue"
"github.com/opentofu/opentofu/internal/lang/eval/internal/tofu2024"
"github.com/opentofu/opentofu/internal/providers"
)
@@ -24,7 +25,11 @@ type Provisioners = evalglue.Provisioners
type ExternalModules = evalglue.ExternalModules
func ModulesForTesting(modules map[addrs.ModuleSourceLocal]*configs.Module) ExternalModules {
return evalglue.ModulesForTesting(modules)
// This one actually lives in tofu2024 because evalglue isn't allowed to
// depend on tofu2024 itself, but from the caller's perspective this is
// still presented as an evalglue re-export because the return type belongs
// to that package.
return tofu2024.ModulesForTesting(modules)
}
func ProvidersForTesting(schemas map[addrs.Provider]*providers.GetProviderSchemaResponse) Providers {

View File

@@ -0,0 +1,89 @@
// 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"
"maps"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/instances"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// singleInstanceSelectorForTesting returns an [InstanceSelector] that
// reports a single no-key instance, similar to when there are no repetition
// arguments at all or when "enabled = true".
func singleInstanceSelectorForTesting() InstanceSelector {
return &instanceSelectorForTesting{
keyType: addrs.NoKeyType,
instances: map[addrs.InstanceKey]instances.RepetitionData{
addrs.NoKey: instances.RepetitionData{},
},
}
}
// disabledInstanceSelectorForTesting returns an [InstanceSelector] that
// mimics what happens when "enabled = false".
func disabledInstanceSelectorForTesting() InstanceSelector {
return &instanceSelectorForTesting{
keyType: addrs.NoKeyType,
instances: nil,
}
}
// countInstanceSelectorForTesting returns an [InstanceSelector] that behaves
// similarly to how we treat the "count" meta-argument.
func countInstanceSelectorForTesting(count int) InstanceSelector {
insts := make(map[addrs.InstanceKey]instances.RepetitionData, count)
for i := range count {
insts[addrs.IntKey(i)] = instances.RepetitionData{
CountIndex: cty.NumberIntVal(int64(i)),
}
}
return &instanceSelectorForTesting{
keyType: addrs.IntKeyType,
instances: insts,
}
}
// countInstanceSelectorForTesting returns an [InstanceSelector] that behaves
// similarly to how we treat the "for_each" meta-argument.
func forEachInstanceSelectorForTesting(elems map[string]cty.Value) InstanceSelector {
insts := make(map[addrs.InstanceKey]instances.RepetitionData, len(elems))
for key, val := range elems {
insts[addrs.StringKey(key)] = instances.RepetitionData{
EachKey: cty.StringVal(key),
EachValue: val,
}
}
return &instanceSelectorForTesting{
keyType: addrs.StringKeyType,
instances: insts,
}
}
type instanceSelectorForTesting struct {
keyType addrs.InstanceKeyType
instances map[addrs.InstanceKey]instances.RepetitionData
instancesMarks cty.ValueMarks
}
// InstanceKeyType implements InstanceSelector.
func (i *instanceSelectorForTesting) InstanceKeyType() addrs.InstanceKeyType {
return i.keyType
}
// Instances implements InstanceSelector.
func (i *instanceSelectorForTesting) Instances(ctx context.Context) (Maybe[InstancesSeq], cty.ValueMarks, tfdiags.Diagnostics) {
return Known(InstancesSeq(maps.All(i.instances))), i.instancesMarks, nil
}
// InstancesSourceRange implements InstanceSelector.
func (i *instanceSelectorForTesting) InstancesSourceRange() *tfdiags.SourceRange {
return nil
}

View File

@@ -7,11 +7,17 @@ package configgraph
import (
"context"
"errors"
"fmt"
"maps"
"slices"
"strings"
"github.com/apparentlymart/go-versions/versions"
"github.com/apparentlymart/go-workgraph/workgraph"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/instances"
@@ -24,10 +30,42 @@ type ModuleCall struct {
Addr addrs.AbsModuleCall
DeclRange tfdiags.SourceRange
// ParentSourceAddr is the source address of the module that contained
// this module call, which is then used as the base for resolving
// any relative addresses returned from SourceAddrValuer.
ParentSourceAddr addrs.ModuleSource
// InstanceSelector represents a rule for deciding which instances of
// this resource have been declared.
InstanceSelector InstanceSelector
// SourceAddrValuer and VersionConstraintValuer together describe how
// to select the module to be called.
//
// We currently require these to be equal for all instances of the
// module call because although in principle this new evaluation model
// could support entirely different declarations in each module, the
// surface syntax of HCL would make that very hard to use (can't easily
// set drastically different arguments for each instance) and this
// also allows us to echo the design for resource instances where we've
// effectively already baked in what schema we ought to be validating
// against before we try to evaluate the config body inside
// [ModuleCallInstance].
SourceAddrValuer *OnceValuer
VersionConstraintValuer *OnceValuer
// ValidateSourceArguments is a callback function provided by whatever
// compiled this [ModuleCall] object that checks whether the source
// arguments are resolvable in the current execution context, so that we
// can report any problems just once at the call level rather than
// re-reporting the same problems once for each instance.
//
// Depending on what phase we're in this could either try to find a
// suitable module in a local cache directory or could even try to actually
// fetch a remote module over the network, and so this function may take
// a long time to return.
ValidateSourceArguments func(ctx context.Context, sourceArgs ModuleSourceArguments) tfdiags.Diagnostics
// 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
@@ -36,7 +74,11 @@ type ModuleCall struct {
// 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
//
// This should only be called with a [ModuleSourceArguments] that was
// accepted by [ModuleCall.ValidateSourceArguments] without returning any
// errors.
CompileCallInstance func(ctx context.Context, sourceArgs ModuleSourceArguments, 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.
@@ -57,9 +99,173 @@ func (c *ModuleCall) Instances(ctx context.Context) map[addrs.InstanceKey]*Modul
return result.Instances
}
func (c *ModuleCall) SourceArguments(ctx context.Context) (Maybe[ModuleSourceArguments], cty.ValueMarks, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
sourceVal, moreDiags := c.SourceAddrValuer.Value(ctx)
diags = diags.Append(moreDiags)
versionVal, moreDiags := c.VersionConstraintValuer.Value(ctx)
diags = diags.Append(moreDiags)
if diags.HasErrors() {
// Not even valid enough to try anything else.
return nil, nil, diags
}
// We'll decode both source address and version together so that we can
// always return their combined marks even if one is invalid.
const sourceDiagSummary = "Invalid module source address"
const versionDiagSummary = "Invalid module version constraints"
sourceStr, sourceMarks, sourceErr := decodeModuleCallSourceArgumentString(sourceVal, false)
versionStr, versionMarks, versionErr := decodeModuleCallSourceArgumentString(versionVal, true)
allMarks := make(cty.ValueMarks)
maps.Copy(allMarks, sourceMarks)
maps.Copy(allMarks, versionMarks)
if sourceErr != nil {
var detail string
switch err := sourceErr.(type) {
case moduleSourceDependsOnResourcesError:
var buf strings.Builder
fmt.Fprintln(&buf, "The module source address value is derived from the results of the following resource instances:")
for _, addr := range err.instAddrs {
fmt.Fprintf(&buf, " - %s\n", addr)
}
fmt.Fprint(&buf, "\n\nModule source selections cannot be based on resource results because they must remain consistent throughout each plan/apply round.")
detail = buf.String()
case moduleSourceDependsOnUpstreamEvalError:
// We intentionally don't generate any more diagnostics in this
// case because we assume that something upstream will already
// have reported error diagnostics for whatever caused this
// error.
default:
detail = fmt.Sprintf("Unsuitable value for module source address: %s.", tfdiags.FormatError(err))
}
if detail != "" {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: sourceDiagSummary,
Detail: detail,
Subject: MaybeHCLSourceRange(c.SourceAddrValuer.ValueSourceRange()),
})
}
}
if versionErr != nil {
var detail string
switch err := sourceErr.(type) {
case moduleSourceDependsOnResourcesError:
var buf strings.Builder
fmt.Fprintln(&buf, "The module version constraints value is derived from the results of the following resource instances:")
for _, addr := range err.instAddrs {
fmt.Fprintf(&buf, " - %s\n", addr)
}
fmt.Fprint(&buf, "\n\nModule source selections cannot be based on resource results because they must remain consistent throughout each plan/apply round.")
detail = buf.String()
case moduleSourceDependsOnUpstreamEvalError:
// We intentionally don't generate any more diagnostics in this
// case because we assume that something upstream will already
// have reported error diagnostics for whatever caused this
// error.
default:
detail = fmt.Sprintf("Unsuitable value for module version constraints: %s.", tfdiags.FormatError(err))
}
if detail != "" {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: versionDiagSummary,
Detail: detail,
Subject: MaybeHCLSourceRange(c.SourceAddrValuer.ValueSourceRange()),
})
}
}
if diags.HasErrors() {
return nil, allMarks, diags
}
sourceAddr, err := addrs.ParseModuleSource(sourceStr)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: sourceDiagSummary,
Detail: fmt.Sprintf("Cannot use %q as module source address: %s.", sourceStr, tfdiags.FormatError(err)),
Subject: MaybeHCLSourceRange(c.SourceAddrValuer.ValueSourceRange()),
})
return nil, allMarks, diags
}
// If the specified source address is a relative path then we need to
// resolve it to absolute based on the source address where this
// module call appeared.
sourceAddr, err = addrs.ResolveRelativeModuleSource(c.ParentSourceAddr, sourceAddr)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: sourceDiagSummary,
Detail: fmt.Sprintf("Cannot use %q as module source address: %s.", sourceStr, tfdiags.FormatError(err)),
Subject: MaybeHCLSourceRange(c.SourceAddrValuer.ValueSourceRange()),
})
return nil, allMarks, diags
}
// FIXME: It would be better if the rule for what source address types
// are allowed to have version constraints would live somewhere else,
// such as in "package addrs".
var allowedVersions versions.Set
if _, isRegistry := sourceAddr.(addrs.ModuleSourceRegistry); isRegistry {
if versionStr != "" {
vs, err := versions.MeetingConstraintsStringRuby(versionStr)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: versionDiagSummary,
Detail: fmt.Sprintf("Cannot use %q as module version constraints: %s.", versionStr, tfdiags.FormatError(err)),
Subject: MaybeHCLSourceRange(c.VersionConstraintValuer.ValueSourceRange()),
})
return nil, allMarks, diags
}
allowedVersions = vs
} else {
allowedVersions = versions.All
}
} else {
if versionStr != "" {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: versionDiagSummary,
Detail: fmt.Sprintf("Module source address %q does not support version constraints.", sourceAddr),
Subject: MaybeHCLSourceRange(c.VersionConstraintValuer.ValueSourceRange()),
})
return nil, allMarks, diags
}
}
return Known(ModuleSourceArguments{
Source: sourceAddr,
AllowedVersions: allowedVersions,
}), allMarks, diags
}
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)
// We intentionally ignore diagnostics and marks here because Value
// deals with those and skips calling this function at all when
// the arguments are too invalid.
maybeSourceArgs, _, _ := c.SourceArguments(ctx)
sourceArgs, ok := GetKnown(maybeSourceArgs)
if !ok {
// For our purposes here we just use this as a signal that we
// should not even try to select instances. [ModuleCall.Value]
// also checks this and skips even making use of the instances
// when the source arguments are invalid, and so we're just
// returning something valid enough for other callers to unwind
// successfully and move on here.
return &compiledInstances[*ModuleCallInstance]{
KeyType: addrs.UnknownKeyType,
Instances: nil,
ValueMarks: nil,
}, nil
}
return compileInstances(ctx, c.InstanceSelector, func(ctx context.Context, key addrs.InstanceKey, repData instances.RepetitionData) *ModuleCallInstance {
return c.CompileCallInstance(ctx, sourceArgs, key, repData)
})
})
}
@@ -70,8 +276,35 @@ func (c *ModuleCall) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagn
// Value implements exprs.Valuer.
func (c *ModuleCall) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
// We'll first check whether the arguments specifying which module to
// call are valid, because we can't really do anything else if not.
maybeSourceArgs, sourceMarks, diags := c.SourceArguments(ctx)
sourceArgs, ok := GetKnown(maybeSourceArgs)
if !ok {
// Either the source information was invalid or was based on something
// that failed evaluation upstream, so we'll just bail out here.
// NOTE: It's possible for sourceArgs to be nil while diags does not
// have errors: we react in this way when the source information is
// unknown because it was derived from something that failed upstream,
// with the assumption that the upstream error generated its own
// diagnostics that'll come via another return path.
return cty.DynamicVal.WithMarks(sourceMarks), diags
}
// We'll validate that the source arguments are acceptable before we
// try to instantiate any instances, because otherwise we'd likely
// detect and report the same problems separately for each instance.
moreDiags := c.ValidateSourceArguments(ctx, sourceArgs)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return cty.DynamicVal.WithMarks(sourceMarks), diags
}
// If the source args were acceptable then we'll decide what instances
// we have and then collect their individual results into our overall
// return value.
selection, diags := c.decideInstances(ctx)
return valueForInstances(ctx, selection), diags
return valueForInstances(ctx, selection).WithMarks(sourceMarks), diags
}
// ValueSourceRange implements exprs.Valuer.
@@ -95,6 +328,15 @@ func (c *ModuleCall) CheckAll(ctx context.Context) tfdiags.Diagnostics {
}
func (c *ModuleCall) AnnounceAllGraphevalRequests(announce func(workgraph.RequestID, grapheval.RequestInfo)) {
announce(c.SourceAddrValuer.RequestID(), grapheval.RequestInfo{
Name: c.Addr.String() + " source address",
SourceRange: c.SourceAddrValuer.ValueSourceRange(),
})
announce(c.VersionConstraintValuer.RequestID(), grapheval.RequestInfo{
Name: c.Addr.String() + " version constraint",
SourceRange: c.VersionConstraintValuer.ValueSourceRange(),
})
// 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.
@@ -113,3 +355,74 @@ func (c *ModuleCall) AnnounceAllGraphevalRequests(announce func(workgraph.Reques
inst.AnnounceAllGraphevalRequests(announce)
}
}
// decodeModuleCallSourceArgumentString deals with the various requirements
// that apply to both the source address _and_ version constraints strings
// in a module call, producing an error that can be used as part of an
// error diagnostic saying that the argument is invalid.
//
// If nullAllowed is true then a null value is represented by returning an
// empty string. An _actual_ empty string is always rejected as invalid input,
// so a successful result with an empty string always means that the given
// value was null.
func decodeModuleCallSourceArgumentString(v cty.Value, nullAllowed bool) (string, cty.ValueMarks, error) {
v, err := convert.Convert(v, cty.String)
retV, retMarks := v.Unmark()
if err != nil {
return "", nil, err
}
if v.IsNull() {
if nullAllowed {
// Note that this is the only case where the result can be ""
// without returning an error, because we reject an _actual_
// empty string with an error later in this function.
return "", retMarks, nil
}
return "", retMarks, errors.New("value must not be null")
}
if ris := slices.Collect(ResourceInstanceAddrs(ContributingResourceInstances(v))); len(ris) != 0 {
// A module source argument is never allowed to depend on a resource
// instance result, even if its value happens to be known in the
// current evaluation context.
return "", retMarks, moduleSourceDependsOnResourcesError{ris}
}
if !v.IsKnown() {
// Although resource instance references are the main reason for
// it to be unknown, it could also be unknown if it were derived
// from an impure function during the planning phase, or from an
// input variable that was set to an unknown value.
//
// We use a special error type when the EvalError mark is present
// because in that case we want to be quiet and avoid distracting
// the user with an error message about unknown values when the
// unknown values were likely generated by OpenTofu itself as
// an implementation detail, rather than by a module authoring mistake.
if exprs.IsEvalError(v) {
return "", nil, moduleSourceDependsOnUpstreamEvalError{}
}
return "", retMarks, errors.New("depends on a value that won't be known until the apply phase")
}
ret := retV.AsString()
if ret == "" {
return "", retMarks, errors.New("must be a non-empty string")
}
return ret, retMarks, nil
}
type moduleSourceDependsOnResourcesError struct {
instAddrs []addrs.AbsResourceInstance
}
// Error implements error, though it returns only a generic error message
// that should be substituted for something better when building a user-facing
// diagnostic,
func (m moduleSourceDependsOnResourcesError) Error() string {
return "must not be derived from resource instance attributes"
}
type moduleSourceDependsOnUpstreamEvalError struct{}
// Error implements error.
func (m moduleSourceDependsOnUpstreamEvalError) Error() string {
return "derived from upstream expression that was invalid"
}

View File

@@ -30,32 +30,9 @@ type ModuleCallInstance struct {
// 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
// Glue is provided by whatever compiled this object to allow us to learn
// more about the module that is being called.
Glue ModuleCallInstanceGlue
// InputsValuer is a valuer for all of the input variable values taken
// together as a single object. It's structured this way mainly for
@@ -65,26 +42,45 @@ type ModuleCallInstance struct {
// that allows constructing the entire map dynamically using expression
// syntax.
InputsValuer *OnceValuer
// TODO: Also something for the "providers side-channel", as represented
// by the "providers" meta-argument in the current language.
}
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".
// We don't perform any static type checking of accesses to a module's
// output value. Instead, we just wait until we have the final result
// in Value.
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")
inputsVal, diags := m.InputsValuer.Value(ctx)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
moreDiags := m.Glue.ValidateInputs(ctx, inputsVal)
// FIXME: Our "contextual diagnostics" mechanism, where the callee provides
// an attribute path and then the caller discovers a suitable source range
// for each diagnostic based on information in the body, can only work
// when we have direct access to a [hcl.Body], but we intentionally
// abstracted that away here. We'll need to find a different design for
// contextual diagnostics that can work through the [exprs.Valuer]
// abstraction to make a best effort to interpret attribute paths against
// whatever the valuer was evaluating.
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return cty.DynamicVal, diags
}
// The actual result value is decided by our caller, which is expected
// to know how to actually find, compile, and evaluate the target module.
return exprs.EvalResult(m.Glue.OutputsValue(ctx, inputsVal))
}
// ValueSourceRange implements exprs.Valuer.
@@ -100,14 +96,6 @@ func (c *ModuleCallInstance) CheckAll(ctx context.Context) tfdiags.Diagnostics {
}
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(),

View File

@@ -0,0 +1,62 @@
// 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-versions/versions"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// ModuleCallInstanceGlue describes a callback API that [ModuleCallInstance]
// objects use to ask the caller questions about the module that's being
// called.
//
// Real implementations of this interface will sometimes block on fetching
// a remote module package for inspection, or on operations caused by
// declarations in the child module. If that external work depends on
// information coming from any other part of this package's API then the
// implementation of that MUST use the mechanisms from package grapheval in
// order to cooperate with the self-dependency detection used by this package to
// prevent deadlocks.
type ModuleCallInstanceGlue interface {
// ValidateInputs determines whether the given value is a valid
// representation of the inputs to the target module, returning diagnostics
// describing any problems.
//
// TODO: This probably also needs an argument for describing the
// "sidechannel" provider instances, as would be specified in the "providers"
// meta-argument in the current language, so the callee can also check
// those.
ValidateInputs(ctx context.Context, inputsVal cty.Value) tfdiags.Diagnostics
// OutputsValue returns the value representing the outputs of this module
// instance. This is what should be returned as the value of the module
// instance.
//
// TODO: This probably also needs an argument for passing the "sidechannel"
// provider instance values.
OutputsValue(ctx context.Context, inputsVal cty.Value) (cty.Value, tfdiags.Diagnostics)
}
type ModuleSourceArguments struct {
// Source is the already-parsed-and-normalized module source address.
Source addrs.ModuleSource
// AllowedVersions describes what subset of the available versions are
// accepted, if the source type is one that supports version constraints.
//
// It's the responsibility of the [ModuleCall] logic to reject attempts
// to set a version constraint for a source type that doesn't support
// it, so a [ModuleSourceArguments] object should not be constructed
// with a nonzero value in this field when [Source] is not of a
// version-aware source type.
AllowedVersions versions.Set
}

View File

@@ -0,0 +1,35 @@
// 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/opentofu/opentofu/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
type moduleInstanceGlueForTesting struct {
sourceAddr string
}
func (g *moduleInstanceGlueForTesting) ValidateInputs(ctx context.Context, configVal cty.Value) tfdiags.Diagnostics {
return nil
}
func (g *moduleInstanceGlueForTesting) OutputsValue(ctx context.Context, configVal cty.Value) (cty.Value, tfdiags.Diagnostics) {
// This simple test-only "glue" just echoes back the source address and
// config it was given, as a stub for testing [ModuleCall] and
// [ModuleCallInstance] alone without any "real" glue implementation.
//
// If you need something more specific in a particular test then it's
// reasonable to write additional implementations of
// [ModuleCallInstanceGlue].
return cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal(g.sourceAddr),
"config": configVal,
}), nil
}

View File

@@ -0,0 +1,193 @@
// 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"
"testing"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/instances"
"github.com/opentofu/opentofu/internal/lang/exprs"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
func TestModuleCall_Value(t *testing.T) {
testValuer(t, map[string]valuerTest[*ModuleCall]{
"remote source address": {
&ModuleCall{
ParentSourceAddr: addrs.MustParseModuleSource("./"),
InstanceSelector: singleInstanceSelectorForTesting(),
SourceAddrValuer: ValuerOnce(exprs.ConstantValuer(
cty.StringVal("https://example.com/foo"),
)),
VersionConstraintValuer: ValuerOnce(exprs.ConstantValuer(cty.NullVal(cty.String))),
ValidateSourceArguments: func(ctx context.Context, sourceArgs ModuleSourceArguments) tfdiags.Diagnostics {
return nil
},
CompileCallInstance: compileModuleCallInstanceSimpleForTesting,
},
cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("https://example.com/foo"),
"config": cty.EmptyObjectVal,
}),
nil,
},
"registry source address with no version": {
&ModuleCall{
ParentSourceAddr: addrs.MustParseModuleSource("./"),
InstanceSelector: singleInstanceSelectorForTesting(),
SourceAddrValuer: ValuerOnce(exprs.ConstantValuer(
cty.StringVal("example.com/foo/bar/baz"),
)),
VersionConstraintValuer: ValuerOnce(exprs.ConstantValuer(cty.NullVal(cty.String))),
ValidateSourceArguments: func(ctx context.Context, sourceArgs ModuleSourceArguments) tfdiags.Diagnostics {
return nil
},
CompileCallInstance: compileModuleCallInstanceSimpleForTesting,
},
cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("example.com/foo/bar/baz"),
"config": cty.EmptyObjectVal,
}),
nil,
},
"registry source address with version": {
&ModuleCall{
ParentSourceAddr: addrs.MustParseModuleSource("./"),
InstanceSelector: singleInstanceSelectorForTesting(),
SourceAddrValuer: ValuerOnce(exprs.ConstantValuer(
cty.StringVal("example.com/foo/bar/baz"),
)),
VersionConstraintValuer: ValuerOnce(exprs.ConstantValuer(
cty.StringVal("<= 2.0.0"),
)),
ValidateSourceArguments: func(ctx context.Context, sourceArgs ModuleSourceArguments) tfdiags.Diagnostics {
return nil
},
CompileCallInstance: compileModuleCallInstanceSimpleForTesting,
},
cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("example.com/foo/bar/baz"),
"config": cty.EmptyObjectVal,
}),
nil,
},
"local source address relative to local": {
&ModuleCall{
ParentSourceAddr: addrs.MustParseModuleSource("./modules/beep"),
InstanceSelector: singleInstanceSelectorForTesting(),
SourceAddrValuer: ValuerOnce(exprs.ConstantValuer(
cty.StringVal("../boop"),
)),
VersionConstraintValuer: ValuerOnce(exprs.ConstantValuer(cty.NullVal(cty.String))),
ValidateSourceArguments: func(ctx context.Context, sourceArgs ModuleSourceArguments) tfdiags.Diagnostics {
return nil
},
CompileCallInstance: compileModuleCallInstanceSimpleForTesting,
},
cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("./modules/boop"),
"config": cty.EmptyObjectVal,
}),
nil,
},
"local source address relative to remote": {
&ModuleCall{
ParentSourceAddr: addrs.MustParseModuleSource("https://example.com/foo.tar.gz//beep/beep"),
InstanceSelector: singleInstanceSelectorForTesting(),
SourceAddrValuer: ValuerOnce(exprs.ConstantValuer(
cty.StringVal("../boop"),
)),
VersionConstraintValuer: ValuerOnce(exprs.ConstantValuer(cty.NullVal(cty.String))),
ValidateSourceArguments: func(ctx context.Context, sourceArgs ModuleSourceArguments) tfdiags.Diagnostics {
return nil
},
CompileCallInstance: compileModuleCallInstanceSimpleForTesting,
},
cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("https://example.com/foo.tar.gz//beep/boop"),
"config": cty.EmptyObjectVal,
}),
nil,
},
"local source address relative to remote escapes": {
&ModuleCall{
ParentSourceAddr: addrs.MustParseModuleSource("https://example.com/foo.tar.gz//beep/beep"),
InstanceSelector: singleInstanceSelectorForTesting(),
SourceAddrValuer: ValuerOnce(exprs.ConstantValuer(
cty.StringVal("../../../outside"),
)),
VersionConstraintValuer: ValuerOnce(exprs.ConstantValuer(cty.NullVal(cty.String))),
ValidateSourceArguments: func(ctx context.Context, sourceArgs ModuleSourceArguments) tfdiags.Diagnostics {
return nil
},
CompileCallInstance: compileModuleCallInstanceSimpleForTesting,
},
cty.DynamicVal,
diagsForTest(tfdiags.Sourceless(
tfdiags.Error,
"Invalid module source address",
`Cannot use "../../../outside" as module source address: invalid relative path from https://example.com/foo.tar.gz//beep/beep: relative path ../../../outside has too many "../" segments.`,
)),
},
"local source address relative to registry": {
&ModuleCall{
ParentSourceAddr: addrs.MustParseModuleSource("example.com/foo/bar/baz//beep/beep"),
InstanceSelector: singleInstanceSelectorForTesting(),
SourceAddrValuer: ValuerOnce(exprs.ConstantValuer(
cty.StringVal("../boop"),
)),
VersionConstraintValuer: ValuerOnce(exprs.ConstantValuer(cty.NullVal(cty.String))),
ValidateSourceArguments: func(ctx context.Context, sourceArgs ModuleSourceArguments) tfdiags.Diagnostics {
return nil
},
CompileCallInstance: compileModuleCallInstanceSimpleForTesting,
},
cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("example.com/foo/bar/baz//beep/boop"),
"config": cty.EmptyObjectVal,
}),
nil,
},
"local source address relative to registry escapes": {
&ModuleCall{
ParentSourceAddr: addrs.MustParseModuleSource("example.com/foo/bar/baz//beep/beep"),
InstanceSelector: singleInstanceSelectorForTesting(),
SourceAddrValuer: ValuerOnce(exprs.ConstantValuer(
cty.StringVal("../../../outside"),
)),
VersionConstraintValuer: ValuerOnce(exprs.ConstantValuer(cty.NullVal(cty.String))),
ValidateSourceArguments: func(ctx context.Context, sourceArgs ModuleSourceArguments) tfdiags.Diagnostics {
return nil
},
CompileCallInstance: compileModuleCallInstanceSimpleForTesting,
},
cty.DynamicVal,
diagsForTest(tfdiags.Sourceless(
tfdiags.Error,
"Invalid module source address",
`Cannot use "../../../outside" as module source address: invalid relative path from example.com/foo/bar/baz//beep/beep: relative path ../../../outside has too many "../" segments.`,
)),
},
})
}
// compileModuleCallInstanceSimpleForTesting is a simple implementation of
// [ModuleCall.CompileCallInstance] that just returns a stub
// [ModuleCallInstance] with its glue set to an instance of
// [moduleInstanceGlueForTesting], so that the instance's result value
// will be an object with "source" and "config" attributes just echoing
// back the source that was passed in with an empty config.
func compileModuleCallInstanceSimpleForTesting(ctx context.Context, sourceArgs ModuleSourceArguments, key addrs.InstanceKey, repData instances.RepetitionData) *ModuleCallInstance {
return &ModuleCallInstance{
Glue: &moduleInstanceGlueForTesting{
sourceAddr: sourceArgs.Source.String(),
},
InputsValuer: ValuerOnce(exprs.ConstantValuer(cty.EmptyObjectVal)),
}
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/tfdiags"
@@ -31,7 +30,7 @@ type ExternalModules interface {
// the implementation can potentially use a lock file to determine which
// version has been selected for that call in particular. forCall is
// nil when requesting the root module.
ModuleConfig(ctx context.Context, source addrs.ModuleSource, allowedVersions versions.Set, forCall *addrs.AbsModuleCall) (*configs.Module, tfdiags.Diagnostics)
ModuleConfig(ctx context.Context, source addrs.ModuleSource, allowedVersions versions.Set, forCall *addrs.AbsModuleCall) (UncompiledModule, tfdiags.Diagnostics)
}
// Providers is implemented by callers of this package to provide access
@@ -120,7 +119,7 @@ func ensureProvisioners(given Provisioners) Provisioners {
}
// ModuleConfig implements ExternalModules.
func (e emptyDependencies) ModuleConfig(ctx context.Context, source addrs.ModuleSource, allowedVersions versions.Set, forCall *addrs.AbsModuleCall) (*configs.Module, tfdiags.Diagnostics) {
func (e emptyDependencies) ModuleConfig(ctx context.Context, source addrs.ModuleSource, allowedVersions versions.Set, forCall *addrs.AbsModuleCall) (UncompiledModule, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,

View File

@@ -9,31 +9,14 @@ import (
"context"
"fmt"
"github.com/apparentlymart/go-versions/versions"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// ModulesForTesting returns an [ExternalModules] implementation that just
// returns module objects directly from the provided map, without any additional
// logic.
//
// This is intended for unit testing only, and only supports local module
// source addresses because it has no means to resolve remote sources or
// selected versions for registry-based modules.
//
// [configs.ModulesFromStringsForTesting] is a convenient way to build a
// suitable map to pass to this function when the required configuration is
// relatively small.
func ModulesForTesting(modules map[addrs.ModuleSourceLocal]*configs.Module) ExternalModules {
return externalModulesStatic{modules}
}
// ProvidersForTesting returns a [Providers] implementation that just returns
// information directly from the given map.
//
@@ -50,26 +33,6 @@ func ProvisionersForTesting(schemas map[string]*configschema.Block) Provisioners
return provisionersStatic{schemas}
}
type externalModulesStatic struct {
modules map[addrs.ModuleSourceLocal]*configs.Module
}
// ModuleConfig implements ExternalModules.
func (ms externalModulesStatic) ModuleConfig(_ context.Context, source addrs.ModuleSource, _ versions.Set, _ *addrs.AbsModuleCall) (*configs.Module, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
localSource, ok := source.(addrs.ModuleSourceLocal)
if !ok {
diags = diags.Append(fmt.Errorf("only local module source addresses are supported for this test"))
return nil, diags
}
ret, ok := ms.modules[localSource]
if !ok {
diags = diags.Append(fmt.Errorf("module path %q is not available to this test", localSource))
return nil, diags
}
return ret, diags
}
type providersStatic struct {
schemas map[addrs.Provider]*providers.GetProviderSchemaResponse
}

View File

@@ -0,0 +1,89 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package evalglue
import (
"context"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/lang/exprs"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// UncompiledModule is an interface implemented by objects representing a
// module that has not yet been "compiled" into a [CompiledModuleInstance].
//
// This provides the facilities needed to validate a module call in preparation
// for using its settings to compile the module, and then for finally
// actually compiling the module into a [CompiledModuleInstance].
//
// This interface abstracts over potentially multiple different implementations
// representing different editions of the OpenTofu language, and so describes
// the API that all editions should agree on to allow modules of different
// editions to coexist and collaborate in a single configuration tree.
type UncompiledModule interface {
// ValidateModuleInputs checks whether the given value is suitable to be
// used as the "inputs" when instantiating the module.
//
// In the current language "inputsVal" should always be of an object type
// whose attributes correspond to the input variables declared inside
// the module.
ValidateModuleInputs(ctx context.Context, inputsVal cty.Value) tfdiags.Diagnostics
// TODO: Something similar to ValidateInputs for the "providers sidechannel"
// so we can know what providers the child module is expecting to be passed
// and thus know what's supposed to be in the "providers" argument of the
// calling module block? Annoying to expose that directly as part of this
// abstraction but probably the most pragmatic way to do it as long as
// the providers sidechannel continues to exist.
// ModuleOutputsTypeConstraint returns the type constraint that the
// outputs value produced by all instances of this module would conform
// to. It's always valid to use the result with `cty.UnknownVal` to produce
// a placeholder for the result of an instance of this module.
//
// In the current language this should always describe an object type whose
// attributes correspond to the output values declared in the module.
ModuleOutputsTypeConstraint(ctx context.Context) cty.Type
// CompileModuleInstance uses the given [ModuleCall] to attempt to compile
// the module into a [CompiledModuleInstance], or returns error diagnostics
// explaining why that isn't possible.
CompileModuleInstance(ctx context.Context, call *ModuleCall) (CompiledModuleInstance, tfdiags.Diagnostics)
}
// ModuleCall represents the information needed to instantiate [UncompiledModule]
// into [CompiledModuleInstance].
type ModuleCall struct {
// InputValues describes the inputs to the module.
//
// The value produced by this valuer should have been previously
// successfully validated using [UncompiledModule.ValidateModuleInputs],
// or compilation is likely to fail with a potentially-confusing error.
InputValues exprs.Valuer
// AllowImpureFunctions controls whether to allow full use of a small
// number of functions that produce different results each time they are
// called, such as "timestamp".
//
// This should typically just be passed on verbatim from an equivalent
// setting in the parent module, because all module instances in a
// configuration instance should agree about whether impure functions
// are active.
AllowImpureFunctions bool
EvaluationGlue Glue
// EvalContext describes the context where the call is being made, dealing
// with cross-cutting concerns like which providers are available and how
// to load them.
//
// This should typically just be passed on verbatim from an equivalent
// setting in the parent module, because EvalContext holds cross-cutting
// concerns from the environment in which OpenTofu is running.
EvalContext *EvalContext
}

View File

@@ -107,7 +107,10 @@ func CompileModuleInstance(
module.ModuleCalls,
topScope,
providersSidechannel,
moduleSourceAddr,
call.CalleeAddr,
call.EvalContext.Modules,
call,
)
ret.resourceNodes = compileModuleInstanceResources(ctx,
module.ManagedResources,
@@ -120,6 +123,15 @@ func CompileModuleInstance(
call.EvaluationGlue.ResourceInstanceValue,
)
// Now that we've assembled all of the innards of the module instance,
// we'll wire the output values up to the top-level module instance
// node so that it can produce the overall result object for this module
// instance.
ret.moduleInstanceNode.OutputValuers = make(map[addrs.OutputValue]*configgraph.OnceValuer, len(ret.outputValueNodes))
for addr, node := range ret.outputValueNodes {
ret.moduleInstanceNode.OutputValuers[addr] = configgraph.ValuerOnce(node)
}
return ret
}
@@ -378,61 +390,107 @@ func compileModuleInstanceModuleCalls(
configs map[string]*configs.ModuleCall,
declScope exprs.Scope,
providersSidechannel *moduleProvidersSideChannel,
parentSourceAddr addrs.ModuleSource,
moduleInstanceAddr addrs.ModuleInstance,
externalModules evalglue.ExternalModules,
parentCall *ModuleInstanceCall,
) map[addrs.ModuleCall]*configgraph.ModuleCall {
ret := make(map[addrs.ModuleCall]*configgraph.ModuleCall, len(configs))
for name, config := range configs {
addr := addrs.ModuleCall{Name: name}
absAddr := addr.Absolute(moduleInstanceAddr)
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))
}
ret[addr] = &configgraph.ModuleCall{
Addr: addr.Absolute(moduleInstanceAddr),
DeclRange: tfdiags.SourceRangeFromHCL(config.DeclRange),
ParentSourceAddr: parentSourceAddr,
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))
SourceAddrValuer: configgraph.ValuerOnce(exprs.NewClosure(
exprs.EvalableHCLExpression(config.Source),
declScope,
)),
VersionConstraintValuer: configgraph.ValuerOnce(
versionConstraintValuer,
),
ValidateSourceArguments: func(ctx context.Context, sourceArgs configgraph.ModuleSourceArguments) tfdiags.Diagnostics {
// We'll try to use the given source address with our
// [ExternalModules] object, and consider the arguments to be
// valid if this succeeds.
//
// If the [ExternalModules] object is one that fetches the
// requested module over the network on first request then we
// expect that it'll cache what it fetched somewhere so that
// a subsequent call with the same arguments will be relatively
// cheap.
_, diags := externalModules.ModuleConfig(ctx, sourceArgs.Source, sourceArgs.AllowedVersions, &absAddr)
return diags
},
CompileCallInstance: func(ctx context.Context, sourceArgs configgraph.ModuleSourceArguments, key addrs.InstanceKey, repData instances.RepetitionData) *configgraph.ModuleCallInstance {
// The contract for [configgraph.ModuleCall] is that it should only
// call this function when sourceArgs is something that was previously
// accepted by [ValidateSourceArguments]. We assume that the module
// dependencies object is doing some sort of caching so that if
// ValidateSourceArguments caused something to be downloaded over
// the network then we can re-request that same object cheaply here.
mod, diags := externalModules.ModuleConfig(ctx, sourceArgs.Source, sourceArgs.AllowedVersions, &absAddr)
if diags.HasErrors() {
// We should not typically find errors here because we would've
// already tried this in ValidateSourceArguments above, but
// we _do_ encounter problems here then we'll return a stubby
// object that just returns whatever diagnostics we found as
// soon as it tries to evaluate its inputs.
return &configgraph.ModuleCallInstance{
ModuleInstanceAddr: addr.Absolute(moduleInstanceAddr).Instance(key),
InputsValuer: configgraph.ValuerOnce(exprs.ForcedErrorValuer(diags)),
Glue: &moduleCallInstanceGlue{
validateInputs: func(ctx context.Context, v cty.Value) tfdiags.Diagnostics {
return diags
},
getOutputsValue: func(ctx context.Context, v cty.Value) (cty.Value, tfdiags.Diagnostics) {
return cty.DynamicVal, diags
},
},
}
}
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.
Glue: &moduleCallInstanceGlue{
validateInputs: func(ctx context.Context, v cty.Value) tfdiags.Diagnostics {
return mod.ValidateModuleInputs(ctx, v)
},
getOutputsValue: func(ctx context.Context, v cty.Value) (cty.Value, tfdiags.Diagnostics) {
modInst, diags := mod.CompileModuleInstance(ctx, &evalglue.ModuleCall{
InputValues: exprs.ConstantValuer(v),
AllowImpureFunctions: parentCall.AllowImpureFunctions,
EvalContext: parentCall.EvalContext,
EvaluationGlue: parentCall.EvaluationGlue,
})
if diags.HasErrors() {
return cty.DynamicVal, diags
}
ret, moreDiags := modInst.ResultValuer(ctx).Value(ctx)
diags = diags.Append(moreDiags)
return ret, diags
},
},
}
},
}
@@ -537,3 +595,16 @@ func (r *resourceInstanceGlue) ValidateConfig(ctx context.Context, configVal cty
func (r *resourceInstanceGlue) ResultValue(ctx context.Context, configVal cty.Value, providerInst configgraph.Maybe[*configgraph.ProviderInstance]) (cty.Value, tfdiags.Diagnostics) {
return r.getResultValue(ctx, configVal, providerInst)
}
type moduleCallInstanceGlue struct {
validateInputs func(context.Context, cty.Value) tfdiags.Diagnostics
getOutputsValue func(context.Context, cty.Value) (cty.Value, tfdiags.Diagnostics)
}
func (g *moduleCallInstanceGlue) ValidateInputs(ctx context.Context, inputsVal cty.Value) tfdiags.Diagnostics {
return g.validateInputs(ctx, inputsVal)
}
func (g *moduleCallInstanceGlue) OutputsValue(ctx context.Context, inputsVal cty.Value) (cty.Value, tfdiags.Diagnostics) {
return g.getOutputsValue(ctx, inputsVal)
}

View File

@@ -23,6 +23,32 @@ import (
_ "github.com/opentofu/opentofu/internal/configs"
)
// === SOME HISTORICAL NOTES ===
//
// For those who are coming here with familiarity with the original runtime
// in "package tofu", you might like to think of the types in this package as
// being _roughly_ analogous to the "graph builder" mechanism in package tofu.
//
// There are some notable differences that are worth knowing before you dive
// in here, though:
//
// - The "compile" code here is intentionally written as much as possible as
// straight-through code that runs to completion and returns a value, whereas
// package tofu's graph builders instead follow an inversion-of-control style
// where a bunch of transformers are run sequentially and each make arbitrary
// modifications to a shared mutable data structure.
// - The "graph" that this code is building is based on the types in the sibling
// package "configgraph", which at the time of writing has its own "historical
// notes" like this describing how it relates to the traditional graph model.
// - An express goal of this "compiler" layer is to create an abstraction
// boundary between the current surface language, presently implemented in
// "package configs", and the execution engine which ideally cares only about
// the relationships between objects and the values flowing between them.
// Therefore nothing in package configgraph should depend on anything from
// package configs, and configgraph should also only be using HCL directly for
// some ancillary concepts like diagnostics and traversals, and even those
// maybe we'll replace with some OpenTofu-specific wrapper types in future.
// Temporary note about possible future plans:
//
// Currently this package is working with [configs.Module] and the other types

View File

@@ -207,14 +207,17 @@ func (m *moduleInstanceScope) resolveSimpleChildAttr(topSymbol string, ref hcl.T
return exprs.ValueOf(v), diags
case "module":
// TODO: Handle this
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Module call references not yet supported",
Detail: "This experimental new implementation does not yet support referring to module calls.",
Subject: &ref.SrcRange,
})
return nil, diags
v, ok := m.inst.moduleCallNodes[addrs.ModuleCall{Name: ref.Name}]
if !ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to undeclared module call",
Detail: fmt.Sprintf("There is no module call named %q declared in this module.", ref.Name),
Subject: &ref.SrcRange,
})
return nil, diags
}
return exprs.ValueOf(v), diags
default:
// We should not get here because there should be a case above for

View File

@@ -0,0 +1,55 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package tofu2024
import (
"context"
"fmt"
"github.com/apparentlymart/go-versions/versions"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/lang/eval/internal/evalglue"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// This file contains test helpers for use in other packages. These functions
// must not be used in non-test code.
// ModulesForTesting returns an [evalglue.ExternalModules] implementation that just
// returns module objects directly from the provided map, without any additional
// logic.
//
// This is intended for unit testing only, and only supports local module
// source addresses because it has no means to resolve remote sources or
// selected versions for registry-based modules.
//
// [configs.ModulesFromStringsForTesting] is a convenient way to build a
// suitable map to pass to this function when the required configuration is
// relatively small.
func ModulesForTesting(modules map[addrs.ModuleSourceLocal]*configs.Module) evalglue.ExternalModules {
return externalModulesStatic{modules}
}
type externalModulesStatic struct {
modules map[addrs.ModuleSourceLocal]*configs.Module
}
// ModuleConfig implements ExternalModules.
func (ms externalModulesStatic) ModuleConfig(_ context.Context, source addrs.ModuleSource, _ versions.Set, _ *addrs.AbsModuleCall) (evalglue.UncompiledModule, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
localSource, ok := source.(addrs.ModuleSourceLocal)
if !ok {
diags = diags.Append(fmt.Errorf("only local module source addresses are supported for this test"))
return nil, diags
}
mod, ok := ms.modules[localSource]
if !ok {
diags = diags.Append(fmt.Errorf("module path %q is not available to this test", localSource))
return nil, diags
}
return NewUncompiledModule(source, mod), diags
}

View File

@@ -0,0 +1,112 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package tofu2024
import (
"context"
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/lang/eval/internal/evalglue"
"github.com/opentofu/opentofu/internal/tfdiags"
)
type uncompiledModule struct {
sourceAddr addrs.ModuleSource
mod *configs.Module
}
func NewUncompiledModule(sourceAddr addrs.ModuleSource, mod *configs.Module) evalglue.UncompiledModule {
return &uncompiledModule{sourceAddr, mod}
}
// ValidateModuleInputs implements evalglue.UncompiledModule.
func (u *uncompiledModule) ValidateModuleInputs(ctx context.Context, inputsVal cty.Value) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
ty := inputsVal.Type()
if !ty.IsObjectType() {
// NOTE: As long as the rest of the system is building inputsVal by
// decoding an HCL body we shouldn't be able to get here, so this
// case is just here for robustness in case we change some assumptions
// in future.
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid module inputs value",
"Module inputs must be represented as an object whose attributes correspond to the child module's input variables.",
nil, // empty path representing that the top-level object is invalid
))
}
decls := u.mod.Variables
for name, decl := range decls {
if !ty.HasAttribute(name) {
if decl.Required() {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Missing definition for required input variable",
fmt.Sprintf("The child module requires a value for the input variable %q.", name),
cty.GetAttrPath(name),
))
}
continue
}
v := inputsVal.GetAttr(name)
wantTy := decl.ConstraintType
_, err := convert.Convert(v, wantTy)
if err != nil {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid value for input variable",
fmt.Sprintf("Unsuitable value for input variable %q: %s.", name, tfdiags.FormatError(err)),
cty.GetAttrPath(name),
))
}
}
for name := range ty.AttributeTypes() {
_, declared := decls[name]
if !declared {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Definition for undeclared input variable",
fmt.Sprintf("The child module has no input variable named %q.", name),
cty.GetAttrPath(name),
))
}
}
return diags
}
// ModuleOutputsTypeConstraint implements evalglue.UncompiledModule.
func (u *uncompiledModule) ModuleOutputsTypeConstraint(ctx context.Context) cty.Type {
atys := make(map[string]cty.Type)
for name := range u.mod.Outputs {
// We don't currently support type constraints on output values, so
// they are always cty.DynamicPseudoType to represent what the surface
// language calls "any".
atys[name] = cty.DynamicPseudoType
}
return cty.Object(atys)
}
// CompileModuleInstance implements evalglue.UncompiledModule.
func (u *uncompiledModule) CompileModuleInstance(ctx context.Context, call *evalglue.ModuleCall) (evalglue.CompiledModuleInstance, tfdiags.Diagnostics) {
rootModuleCall := &ModuleInstanceCall{
CalleeAddr: addrs.RootModuleInstance,
InputValues: call.InputValues,
EvaluationGlue: call.EvaluationGlue,
AllowImpureFunctions: call.AllowImpureFunctions,
EvalContext: call.EvalContext,
}
rootModuleInstance := CompileModuleInstance(ctx, u.mod, u.sourceAddr, rootModuleCall)
return rootModuleInstance, nil
}