mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
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:
@@ -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.
|
||||
|
||||
72
internal/addrs/module_source_resolve.go
Normal file
72
internal/addrs/module_source_resolve.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
89
internal/lang/eval/internal/configgraph/instances_test.go
Normal file
89
internal/lang/eval/internal/configgraph/instances_test.go
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
193
internal/lang/eval/internal/configgraph/module_call_test.go
Normal file
193
internal/lang/eval/internal/configgraph/module_call_test.go
Normal 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)),
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
89
internal/lang/eval/internal/evalglue/module.go
Normal file
89
internal/lang/eval/internal/evalglue/module.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
55
internal/lang/eval/internal/tofu2024/testing.go
Normal file
55
internal/lang/eval/internal/tofu2024/testing.go
Normal 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
|
||||
}
|
||||
112
internal/lang/eval/internal/tofu2024/uncompiled.go
Normal file
112
internal/lang/eval/internal/tofu2024/uncompiled.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user