mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
lang/eval: Beginnings of a different way to handle config eval
In "package tofu" today we try to do everything using a generic acyclic graph model and generic graph walk, which _works_ but tends to make every other part of the problem very hard to follow because we rely a lot on sidecar shared mutable data structures to propagate results between the isolated operations. This is the beginning of an experimental new way to do it where the "graph" is implied by a model that more closely represents how the language itself works, with explicit modelling of the relationships between different types of objects and letting results flow directly from one object to another without any big shared mutable state. There's still a lot to do before this is actually complete enough to evaluate whether it's a viable new design, but I'm considering this a good starting checkpoint since there's enough here to run a simple test of propagating data all the way from input variables to output values via intermediate local values. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
1
go.mod
1
go.mod
@@ -29,6 +29,7 @@ require (
|
||||
github.com/apparentlymart/go-shquot v0.0.1
|
||||
github.com/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13
|
||||
github.com/apparentlymart/go-versions v1.0.3
|
||||
github.com/apparentlymart/go-workgraph v0.0.0-20250609024419-b3453ef8d3e6
|
||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.2
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9
|
||||
|
||||
2
go.sum
2
go.sum
@@ -120,6 +120,8 @@ github.com/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13 h1:Jtue
|
||||
github.com/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13/go.mod h1:7kfpUbyCdGJ9fDRCp3fopPQi5+cKNHgTE4ZuNrO71Cw=
|
||||
github.com/apparentlymart/go-versions v1.0.3 h1:T3b8tumoQLuu1dej2Y9v22J4PWV9IzDLh2A9lIPoVSM=
|
||||
github.com/apparentlymart/go-versions v1.0.3/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM=
|
||||
github.com/apparentlymart/go-workgraph v0.0.0-20250609024419-b3453ef8d3e6 h1:gY9564QrJDDPS2NLN6ySdikfhsN/oFTs/yWa4GIzY0w=
|
||||
github.com/apparentlymart/go-workgraph v0.0.0-20250609024419-b3453ef8d3e6/go.mod h1:I0Pvlprhm5vLDT5v0Tc3Wytw9NRBUSk2aEGTDxFSDDI=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
|
||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
|
||||
85
internal/lang/eval/config.go
Normal file
85
internal/lang/eval/config.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package eval
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/apparentlymart/go-versions/versions"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/lang/eval/internal/configgraph"
|
||||
"github.com/opentofu/opentofu/internal/lang/exprs"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
// ConfigInstance represents an instance ofan already-assembled configuration
|
||||
// tree, bound to some input variable values and other context that were
|
||||
// provided when it was built.
|
||||
type ConfigInstance struct {
|
||||
rootModuleInstance *configgraph.ModuleInstance
|
||||
}
|
||||
|
||||
// ConfigCall describes a call to a root module that acts conceptually like
|
||||
// a "module" block but is instead implied by something outside of the
|
||||
// module language itself, such as running an OpenTofu CLI command.
|
||||
type ConfigCall struct {
|
||||
// RootModuleSource is the source address of the root module.
|
||||
//
|
||||
// This must be a source address that can be resolved by the
|
||||
// [ExternalModules] implementation provided in EvalContext.
|
||||
RootModuleSource addrs.ModuleSource
|
||||
|
||||
// InputValues describes how to obtain values for the input variables
|
||||
// declared in the root module.
|
||||
//
|
||||
// In typical use the InputValues map is assembled based on a combination
|
||||
// of ".tfvars" files, CLI arguments, and environment variables, but that's
|
||||
// the responsibility of the Tofu CLI layer and so this package is totally
|
||||
// unopinionated about how those are provided, so e.g. for .tftest.hcl "run"
|
||||
// blocks the input values could come from the test scenario configuration
|
||||
// instead.
|
||||
//
|
||||
// In unit tests where the source of input variables is immaterial,
|
||||
// [InputValuesForTesting] might be useful to build values for this
|
||||
// field inline in the test code.
|
||||
InputValues map[addrs.InputVariable]exprs.Valuer
|
||||
|
||||
// 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.
|
||||
EvalContext *EvalContext
|
||||
}
|
||||
|
||||
// NewConfigInstance builds a new [ConfigInstance] based on the information
|
||||
// in the given [ConfigCall] object.
|
||||
//
|
||||
// If the returned diagnostics has errors then the first result is invalid
|
||||
// and must not be used. Diagnostics returned directly by this function
|
||||
// are focused only on the process of obtaining the root module; all other
|
||||
// problems are deferred until subsequent evaluation.
|
||||
func NewConfigInstance(ctx context.Context, call *ConfigCall) (*ConfigInstance, tfdiags.Diagnostics) {
|
||||
// The following compensations are for the convenience of unit tests, but
|
||||
// real callers should explicitly set all of this.
|
||||
if call.EvalContext == nil {
|
||||
call.EvalContext = &EvalContext{}
|
||||
}
|
||||
call.EvalContext.init()
|
||||
|
||||
evalCtx := call.EvalContext
|
||||
|
||||
rootModule, diags := evalCtx.Modules.ModuleConfig(ctx, call.RootModuleSource, versions.All, nil)
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
rootModuleCall := &moduleInstanceCall{
|
||||
inputValues: call.InputValues,
|
||||
}
|
||||
rootModuleInstance := compileModuleInstance(rootModule, call.RootModuleSource, rootModuleCall, evalCtx)
|
||||
return &ConfigInstance{
|
||||
rootModuleInstance: rootModuleInstance,
|
||||
}, nil
|
||||
}
|
||||
24
internal/lang/eval/config_testing.go
Normal file
24
internal/lang/eval/config_testing.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package eval
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/lang/exprs"
|
||||
)
|
||||
|
||||
// InputValuesForTesting returns input variable definitions based on a constant
|
||||
// map, intended for convenient test setup in unit tests where it only matters
|
||||
// what the variable values are and not how they are provided.
|
||||
func InputValuesForTesting(vals map[string]cty.Value) map[addrs.InputVariable]exprs.Valuer {
|
||||
ret := make(map[addrs.InputVariable]exprs.Valuer, len(vals))
|
||||
for name, val := range vals {
|
||||
ret[addrs.InputVariable{Name: name}] = exprs.ConstantValuer(val)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
45
internal/lang/eval/context.go
Normal file
45
internal/lang/eval/context.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package eval
|
||||
|
||||
// EvalContext is a collection of contextual information provided by an
|
||||
// external caller of this package to help it to interact with the surrounding
|
||||
// environment.
|
||||
type EvalContext struct {
|
||||
// This type should only contain broad stuff that'd typically be set up
|
||||
// only once for a particular OpenTofu CLI command, and NOT
|
||||
// operation-specific things like the input variables provided for a
|
||||
// given module, or where state is supposed to be stored, etc.
|
||||
|
||||
// Modules gives access to all of the modules available for use in
|
||||
// this context.
|
||||
Modules ExternalModules
|
||||
|
||||
// Providers gives access to all of the providers available for use
|
||||
// in this context.
|
||||
Providers Providers
|
||||
|
||||
// Provisioners gives access to all of the provisioners available for
|
||||
// use in this context.
|
||||
Provisioners Provisioners
|
||||
}
|
||||
|
||||
// init must be called early on entry to any exported function that accepts
|
||||
// an [EvalContext] as an argument to prepare it for use, before accessing
|
||||
// any of its fields or calling any of its other methods.
|
||||
func (c *EvalContext) init() {
|
||||
// If any of the external dependency fields were left nil (likely in
|
||||
// unit tests which aren't intending to use a particular kind of dependency)
|
||||
// we'll replace it with a non-nil implementation that just returns an
|
||||
// error immediately on call, so that accidental reliance on these will
|
||||
// return an error instead of panicking.
|
||||
//
|
||||
// "Real" callers (performing operations on behalf of end-users) should
|
||||
// avoid relying on this because it returns low-quality error messages.
|
||||
c.Modules = ensureExternalModules(c.Modules)
|
||||
c.Providers = ensureProviders(c.Providers)
|
||||
c.Provisioners = ensureProvisioners(c.Provisioners)
|
||||
}
|
||||
180
internal/lang/eval/dependencies.go
Normal file
180
internal/lang/eval/dependencies.go
Normal file
@@ -0,0 +1,180 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package eval
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// Modules is implemented by callers of this package to provide access to
|
||||
// the modules needed by a configuration without this package needing to
|
||||
// know anything about how to fetch modules and perform the initial parsing
|
||||
// and static decoding steps for them.
|
||||
type ExternalModules interface {
|
||||
// ModuleConfig finds and loads a module meeting the given constraints.
|
||||
//
|
||||
// OpenTofu allows each module call to have a different version constraint
|
||||
// and selected module version, and so this signature also includes the
|
||||
// address of the module call the request is made on behalf of so that
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Providers is implemented by callers of this package to provide access
|
||||
// to the providers needed by a configuration without this package needing
|
||||
// to know anything about how provider plugins work, or whether plugins are
|
||||
// even being used.
|
||||
type Providers interface {
|
||||
// ProviderConfigSchema returns the schema that should be used to evaluate
|
||||
// a "provider" block associated with the given provider.
|
||||
//
|
||||
// All providers are required to have a config schema, although for some
|
||||
// providers it is completely empty to represent that no explicit
|
||||
// configuration is needed.
|
||||
ProviderConfigSchema(ctx context.Context, provider addrs.Provider) (*providers.Schema, tfdiags.Diagnostics)
|
||||
|
||||
// ValidateProviderConfig runs provider-specific logic to check whether
|
||||
// the given configuration is valid. Returns at least one error diagnostic
|
||||
// if the configuration is not valid, and may also return warning
|
||||
// diagnostics regardless of whether the configuration is valid.
|
||||
//
|
||||
// The given config value is guaranteed to be an object conforming to
|
||||
// the schema returned by a previous call to ProviderConfigSchema for
|
||||
// the same provider.
|
||||
ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics
|
||||
|
||||
// ResourceTypeSchema returns the schema for configuration and state of
|
||||
// a resource of the given type, or nil if the given provider does not
|
||||
// offer any such resource type.
|
||||
//
|
||||
// Returns error diagnostics if the given provider isn't available for use
|
||||
// at all, regardless of the resource type.
|
||||
ResourceTypeSchema(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string) (*providers.Schema, tfdiags.Diagnostics)
|
||||
|
||||
// ValidateResourceConfig runs provider-specific logic to check whether
|
||||
// the given configuration is valid. Returns at least one error diagnostic
|
||||
// if the configuration is not valid, and may also return warning
|
||||
// diagnostics regardless of whether the configuration is valid.
|
||||
//
|
||||
// The given config value is guaranteed to be an object conforming to
|
||||
// the schema returned by a previous call to ResourceTypeSchema for
|
||||
// the same resource type.
|
||||
ValidateResourceConfig(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string, configVal cty.Value) tfdiags.Diagnostics
|
||||
}
|
||||
|
||||
// Providers is implemented by callers of this package to provide access
|
||||
// to the provisioners needed by a configuration.
|
||||
type Provisioners interface {
|
||||
// ProvisionerConfigSchema returns the schema that should be used to
|
||||
// evaluate a "provisioner" block associated with the given provisioner
|
||||
// type, or nil if there is no known provisioner of the given name.
|
||||
ProvisionerConfigSchema(ctx context.Context, typeName string) (*configschema.Block, tfdiags.Diagnostics)
|
||||
}
|
||||
|
||||
// emptyDependencies is an implementation of all of our dependency-related
|
||||
// interfaces at once, in all cases behaving as if nothing exists.
|
||||
//
|
||||
// We use this with [ensureExternalModules], [ensureProviders], and
|
||||
// [ensureProvisioners] to substitute a caller-provided nil implementation
|
||||
// with a non-nil implementation that contains nothing, so that the rest
|
||||
// of the code doesn't need to repeatedly check for and handle nil.
|
||||
//
|
||||
// This returns low-quality error messages not suitable for use in real
|
||||
// situations; it's here primarily for convenience when writing unit tests
|
||||
// which don't make any use of a particular kind of dependency.
|
||||
type emptyDependencies struct{}
|
||||
|
||||
func ensureExternalModules(given ExternalModules) ExternalModules {
|
||||
if given == nil {
|
||||
return emptyDependencies{}
|
||||
}
|
||||
return given
|
||||
}
|
||||
|
||||
func ensureProviders(given Providers) Providers {
|
||||
if given == nil {
|
||||
return emptyDependencies{}
|
||||
}
|
||||
return given
|
||||
}
|
||||
|
||||
func ensureProvisioners(given Provisioners) Provisioners {
|
||||
if given == nil {
|
||||
return emptyDependencies{}
|
||||
}
|
||||
return given
|
||||
}
|
||||
|
||||
// ModuleConfig implements ExternalModules.
|
||||
func (e emptyDependencies) ModuleConfig(ctx context.Context, source addrs.ModuleSource, allowedVersions versions.Set, forCall *addrs.AbsModuleCall) (*configs.Module, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"No modules are available",
|
||||
"There are no modules available for use in this context.",
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// ProviderConfigSchema implements Providers.
|
||||
func (e emptyDependencies) ProviderConfigSchema(ctx context.Context, provider addrs.Provider) (*providers.Schema, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"No providers are available",
|
||||
"There are no providers available for use in this context.",
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// ValidateProviderConfig implements Providers.
|
||||
func (e emptyDependencies) ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics {
|
||||
// Because our ResourceTypeSchema implementation never succeeds, there
|
||||
// can never be a call to this function in practice and so we'll just
|
||||
// do nothing here.
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResourceTypeSchema implements Providers.
|
||||
func (e emptyDependencies) ResourceTypeSchema(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string) (*providers.Schema, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"No providers are available",
|
||||
"There are no providers available for use in this context.",
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// ValidateResourceConfig implements Providers.
|
||||
func (e emptyDependencies) ValidateResourceConfig(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string, configVal cty.Value) tfdiags.Diagnostics {
|
||||
// Because our ResourceTypeSchema implementation never succeeds, there
|
||||
// can never be a call to this function in practice and so we'll just
|
||||
// do nothing here.
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProvisionerConfigSchema implements Provisioners.
|
||||
func (e emptyDependencies) ProvisionerConfigSchema(ctx context.Context, typeName string) (*configschema.Block, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"No provisioners are available",
|
||||
"There are no provisioners available for use in this context.",
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
136
internal/lang/eval/dependencies_testing.go
Normal file
136
internal/lang/eval/dependencies_testing.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package eval
|
||||
|
||||
import (
|
||||
"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.
|
||||
//
|
||||
// This is intended for unit testing only.
|
||||
func ProvidersForTesting(schemas map[addrs.Provider]*providers.GetProviderSchemaResponse) Providers {
|
||||
return providersStatic{schemas}
|
||||
}
|
||||
|
||||
// ProvisionersForTesting returns a [Provisioners] implementation that just
|
||||
// returns information directly from the given map.
|
||||
//
|
||||
// This is intended for unit testing only.
|
||||
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
|
||||
}
|
||||
|
||||
// ProviderConfigSchema implements Providers.
|
||||
func (p providersStatic) ProviderConfigSchema(ctx context.Context, provider addrs.Provider) (*providers.Schema, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
topSchema, ok := p.schemas[provider]
|
||||
if !ok {
|
||||
diags = diags.Append(fmt.Errorf("provider %q is not available to this test", provider))
|
||||
return nil, diags
|
||||
}
|
||||
return &topSchema.Provider, diags
|
||||
}
|
||||
|
||||
// ValidateProviderConfig implements Providers by doing nothing at all, because
|
||||
// in this implementation providers consist only of schema and have no behavior.
|
||||
func (p providersStatic) ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResourceTypeSchema implements Providers.
|
||||
func (p providersStatic) ResourceTypeSchema(_ context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string) (*providers.Schema, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
topSchema, ok := p.schemas[provider]
|
||||
if !ok {
|
||||
diags = diags.Append(fmt.Errorf("provider %q is not available to this test", provider))
|
||||
return nil, diags
|
||||
}
|
||||
var typesOfMode map[string]providers.Schema
|
||||
switch mode {
|
||||
case addrs.ManagedResourceMode:
|
||||
typesOfMode = topSchema.ResourceTypes
|
||||
case addrs.DataResourceMode:
|
||||
typesOfMode = topSchema.DataSources
|
||||
case addrs.EphemeralResourceMode:
|
||||
typesOfMode = topSchema.EphemeralResources
|
||||
default:
|
||||
typesOfMode = nil // no other modes supported
|
||||
}
|
||||
schema, ok := typesOfMode[typeName]
|
||||
if !ok {
|
||||
// The requirements for this interface say we should return nil to
|
||||
// represent that there is no such resource type, so that the caller
|
||||
// can provide its own error message for that.
|
||||
return nil, diags
|
||||
}
|
||||
return &schema, diags
|
||||
}
|
||||
|
||||
// ValidateResourceConfig implements Providers by doing nothing at all, because
|
||||
// in this implementation providers consist only of schema and have no behavior.
|
||||
func (p providersStatic) ValidateResourceConfig(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string, configVal cty.Value) tfdiags.Diagnostics {
|
||||
return nil
|
||||
}
|
||||
|
||||
type provisionersStatic struct {
|
||||
schemas map[string]*configschema.Block
|
||||
}
|
||||
|
||||
// ProvisionerConfigSchema implements Provisioners.
|
||||
func (p provisionersStatic) ProvisionerConfigSchema(_ context.Context, typeName string) (*configschema.Block, tfdiags.Diagnostics) {
|
||||
return p.schemas[typeName], nil
|
||||
}
|
||||
17
internal/lang/eval/doc.go
Normal file
17
internal/lang/eval/doc.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package eval aims to encapsulate the details of evaluating the objects
|
||||
// in an overall configuration, including all of the expressions written inside
|
||||
// their declarations, in a way that can be reused across various different
|
||||
// phases of execution.
|
||||
//
|
||||
// The scope of this package intentionally excludes concepts like prior state,
|
||||
// plans, etc, focusing only on dealing with the relationships between objects
|
||||
// in a configuration. This package is therefore intended to be used as an
|
||||
// implementation detail of higher-level operations like planning, with the
|
||||
// caller using the provided hooks to incorporate the results of side-effects
|
||||
// managed elsewhere.
|
||||
package eval
|
||||
137
internal/lang/eval/internal/configgraph/check_rule.go
Normal file
137
internal/lang/eval/internal/configgraph/check_rule.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
package configgraph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/checks"
|
||||
"github.com/opentofu/opentofu/internal/lang/exprs"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
type CheckRule struct {
|
||||
// Condition is the as-yet-unevaluated expression for deciding whether the
|
||||
// check is satisfied. Evaluation of this is delayed to allow providing
|
||||
// a local scope when the logic in the containing object actually evaluates
|
||||
// the check.
|
||||
Condition exprs.Evalable
|
||||
|
||||
// ErrorMessageRaw is the as-yet-unevaluated expression for producing an
|
||||
// error message when the condition does not pass.
|
||||
ErrorMessageRaw exprs.Evalable
|
||||
|
||||
// ParentScope is the scope where the check rule was declared,
|
||||
// which might need to be wrapped in a local child scope before actually
|
||||
// evaluating the condition and error message.
|
||||
ParentScope exprs.Scope
|
||||
|
||||
// EphemeralAllowed indicates whether the condition and error message are
|
||||
// allowed to be derived from ephemeral values. If not, the relevant
|
||||
// methods will return error diagnostics when ephemeral values emerge.
|
||||
EphemeralAllowed bool
|
||||
|
||||
DeclSourceRange tfdiags.SourceRange
|
||||
}
|
||||
|
||||
func (r *CheckRule) Check(ctx context.Context, scopeBuilder exprs.ChildScopeBuilder) (checks.Status, tfdiags.Diagnostics) {
|
||||
scope := r.childScope(ctx, scopeBuilder)
|
||||
rawResult, diags := exprs.Evaluate(ctx, r.Condition, scope)
|
||||
if diags.HasErrors() {
|
||||
return checks.StatusError, diags
|
||||
}
|
||||
rawResult, err := convert.Convert(rawResult, cty.Bool)
|
||||
if err == nil && rawResult.IsNull() {
|
||||
err = fmt.Errorf("value must not be null")
|
||||
}
|
||||
if err == nil && rawResult.HasMark(marks.Sensitive) {
|
||||
err = fmt.Errorf("must not be derived from a sensitive value")
|
||||
// TODO: Also annotate the diagnostic with the "caused by sensitive"
|
||||
// annotation, so that the diagnostic renderer can describe where
|
||||
// the sensitive values might have come from.
|
||||
}
|
||||
if err == nil && rawResult.HasMark(marks.Ephemeral) && !r.EphemeralAllowed {
|
||||
err = fmt.Errorf("must not be derived from an ephemeral value")
|
||||
}
|
||||
if err != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid check condition",
|
||||
Detail: fmt.Sprintf("Invalid result for check condition expression: %s.", tfdiags.FormatError(err)),
|
||||
Subject: r.Condition.EvalableSourceRange().ToHCL().Ptr(),
|
||||
})
|
||||
return checks.StatusError, diags
|
||||
}
|
||||
if !rawResult.IsKnown() {
|
||||
return checks.StatusUnknown, diags
|
||||
}
|
||||
// TODO: Handle "deprecated" marks, adding any deprecation-related
|
||||
// diagnostics into diags.
|
||||
rawResult, _ = rawResult.Unmark() // marks dealt with above
|
||||
if rawResult.True() {
|
||||
return checks.StatusPass, diags
|
||||
}
|
||||
return checks.StatusFail, diags
|
||||
}
|
||||
|
||||
func (r *CheckRule) ErrorMessage(ctx context.Context, scopeBuilder exprs.ChildScopeBuilder) (string, tfdiags.Diagnostics) {
|
||||
scope := r.childScope(ctx, scopeBuilder)
|
||||
rawResult, diags := exprs.Evaluate(ctx, r.ErrorMessageRaw, scope)
|
||||
if diags.HasErrors() {
|
||||
return "", diags
|
||||
}
|
||||
rawResult, err := convert.Convert(rawResult, cty.String)
|
||||
if err == nil && rawResult.IsNull() {
|
||||
err = fmt.Errorf("value must not be null")
|
||||
}
|
||||
if err == nil && rawResult.HasMark(marks.Sensitive) {
|
||||
err = fmt.Errorf("must not be derived from a sensitive value")
|
||||
// TODO: Also annotate the diagnostic with the "caused by sensitive"
|
||||
// annotation, so that the diagnostic renderer can describe where
|
||||
// the sensitive values might have come from.
|
||||
}
|
||||
if err == nil && rawResult.HasMark(marks.Ephemeral) && !r.EphemeralAllowed {
|
||||
err = fmt.Errorf("must not be derived from an ephemeral value")
|
||||
}
|
||||
if err == nil && !rawResult.IsKnown() {
|
||||
err = fmt.Errorf("derived from value that is not yet known")
|
||||
// TODO: Also annotate the diagnostic with the "caused by unknown"
|
||||
// annotation, so that the diagnostic renderer can describe where
|
||||
// the unknown values might have come from.
|
||||
}
|
||||
if err != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid error message for check",
|
||||
Detail: fmt.Sprintf("Invalid result for error message expression: %s.", tfdiags.FormatError(err)),
|
||||
Subject: r.Condition.EvalableSourceRange().ToHCL().Ptr(),
|
||||
})
|
||||
return "", diags
|
||||
}
|
||||
// TODO: Handle "deprecated" marks, adding any deprecation-related
|
||||
// diagnostics into diags.
|
||||
rawResult, _ = rawResult.Unmark() // marks dealt with above
|
||||
return rawResult.AsString(), diags
|
||||
}
|
||||
|
||||
// ConditionRange returns the source range where the condition expression was declared.
|
||||
func (r *CheckRule) ConditionRange() tfdiags.SourceRange {
|
||||
return r.Condition.EvalableSourceRange()
|
||||
}
|
||||
|
||||
// DeclRange returns the source range where this check was declared.
|
||||
func (r *CheckRule) DeclRange() tfdiags.SourceRange {
|
||||
return r.DeclSourceRange
|
||||
}
|
||||
|
||||
func (r *CheckRule) childScope(ctx context.Context, builder exprs.ChildScopeBuilder) exprs.Scope {
|
||||
return builder.Build(ctx, r.ParentScope)
|
||||
}
|
||||
67
internal/lang/eval/internal/configgraph/diagnostics.go
Normal file
67
internal/lang/eval/internal/configgraph/diagnostics.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// 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 (
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/lang/exprs"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
func maybeHCLSourceRange(maybeRng *tfdiags.SourceRange) *hcl.Range {
|
||||
if maybeRng == nil {
|
||||
return nil
|
||||
}
|
||||
return maybeRng.ToHCL().Ptr()
|
||||
}
|
||||
|
||||
// diagsHandledElsewhere takes the result of a function that returns a value
|
||||
// along with diagnostics and discards the diagnostics, returning the value
|
||||
// possibly marked with [exprs.EvalError].
|
||||
//
|
||||
// Any use of this function should typically have a nearby comment justifying
|
||||
// why it's okay to use. The remainder of this doc comment explains _in general_
|
||||
// what kinds of situations are valid for using this function.
|
||||
//
|
||||
// It only makes sense to use this with return values from functions that
|
||||
// guarantee to return a useful placeholder valueeven when they return error
|
||||
// diagnostics. For example, [exprs.Valuer] and [exprs.Evalable] implementations
|
||||
// are both expected to return an unknown value placeholder suitable for use
|
||||
// in downstream expressions even when they encounter errors.
|
||||
//
|
||||
// This is here to model our pattern where the evaluation of a particular
|
||||
// object or expression should only return diagnostics directly related to
|
||||
// that object or expression, and should not incorporate diagnostics related
|
||||
// to other objects depended on indirectly. This pattern is under the assumption
|
||||
// that all objects will be visited directly during normal processing and
|
||||
// so will get an opportunity to return their own diagnostics at that point.
|
||||
//
|
||||
// The expression evaluator in package exprs automatically handles the most
|
||||
// common case of this pattern where an expression includes a reference to
|
||||
// something which cannot generate its own value without encountering
|
||||
// diagnostics; those indirect diagnostics are already disccarded during
|
||||
// the preparation of the expression's evaluation context. Explicit use of
|
||||
// this function is therefore needed only for situations where direct logic
|
||||
// is in some sense behaving _like_ expression evaluation -- combining
|
||||
// representations of multiple objects from elsewhere into a larger overall
|
||||
// result -- but without going through the expression evaluation machinery
|
||||
// to do it.
|
||||
//
|
||||
// The body of this function is straightforward but we call it intentionally so
|
||||
// that uses of it are clearly connected with this documentation.
|
||||
func diagsHandledElsewhere(v cty.Value, diags tfdiags.Diagnostics) cty.Value {
|
||||
if diags.HasErrors() {
|
||||
// If the value was derived from a failing expression evaluation then
|
||||
// this mark would probably already be present anyway, but we'll
|
||||
// handle it again here just to help get consistent behavior when
|
||||
// we're building values with hand-written logic instead of by
|
||||
// normal expression evaluation.
|
||||
v = v.Mark(exprs.EvalError)
|
||||
}
|
||||
return v
|
||||
}
|
||||
20
internal/lang/eval/internal/configgraph/doc.go
Normal file
20
internal/lang/eval/internal/configgraph/doc.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package configgraph contains the unexported implementation details of
|
||||
// package eval.
|
||||
//
|
||||
// Package eval offers an API focused on what external callers need to
|
||||
// implement specific operations like the plan and apply phases, while
|
||||
// hiding the handling of language features that get treated equivalently
|
||||
// regardless of phase. This package is the main place that such handling is
|
||||
// hidden.
|
||||
//
|
||||
// All functions in this package which take context.Context objects as their
|
||||
// first argument require a context that's derived from a call to
|
||||
// [grapheval.ContextWithWorker], and in non-test situations _also_ one
|
||||
// derived from a call to [grapheval.ContextWithRequestTracker] to allow
|
||||
// identifying failed evaluation requests in error messages.
|
||||
package configgraph
|
||||
189
internal/lang/eval/internal/configgraph/input_variable.go
Normal file
189
internal/lang/eval/internal/configgraph/input_variable.go
Normal file
@@ -0,0 +1,189 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package configgraph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/ext/typeexpr"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/checks"
|
||||
"github.com/opentofu/opentofu/internal/lang/exprs"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
type InputVariable struct {
|
||||
// DeclName is the name of the variable as written in the header of its
|
||||
// declaration block.
|
||||
DeclName string
|
||||
|
||||
// RawValue produces the "raw" value, as chosen by the caller of the
|
||||
// module, which has not yet been type-converted or validated.
|
||||
RawValue exprs.Valuer
|
||||
|
||||
// TargetType and targetDefaults together represent the type conversion
|
||||
// and default object attribute value insertions that must be applied
|
||||
// to rawValue to produce the final result.
|
||||
TargetType cty.Type
|
||||
TargetDefaults *typeexpr.Defaults
|
||||
|
||||
// TODO: Default value
|
||||
// TODO: ForceEphemeral, ForceSensitive
|
||||
|
||||
// ValidationRules are user-defined checks that must succeed for the
|
||||
// final value to be considered valid for use in downstream expressions.
|
||||
//
|
||||
// The checking and error message evaluation for these rules must be
|
||||
// performed in a child scope where the raw value is directly exposed
|
||||
// under the same symbol where it would normally appear, because
|
||||
// otherwise checking these rules would depend on the success of these
|
||||
// very rules and so there would be a self-reference error.
|
||||
ValidationRules []CheckRule
|
||||
}
|
||||
|
||||
var _ exprs.Valuer = (*InputVariable)(nil)
|
||||
|
||||
// StaticCheckTraversal implements exprs.Valuer.
|
||||
func (i *InputVariable) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics {
|
||||
// We're checking against the type constraint that the final value is
|
||||
// guaranteed to conform to, rather than whatever type the raw value
|
||||
// has, because conversion to a target type with optional attributes
|
||||
// can potentially introduce new attributes. However, we need to
|
||||
// discard the optional attribute information first because
|
||||
// exprs.StaticCheckTraversalThroughType wants a type constraint, not
|
||||
// a "target type" for type conversion.
|
||||
typeConstraint := i.TargetType.WithoutOptionalAttributesDeep()
|
||||
return exprs.StaticCheckTraversalThroughType(traversal, typeConstraint)
|
||||
}
|
||||
|
||||
// Value implements exprs.Valuer.
|
||||
func (i *InputVariable) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
|
||||
rawV, diags := i.RawValue.Value(ctx)
|
||||
if i.TargetDefaults != nil {
|
||||
rawV = i.TargetDefaults.Apply(rawV)
|
||||
}
|
||||
finalV, err := convert.Convert(rawV, i.TargetType)
|
||||
if err != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid value for input variable",
|
||||
Detail: fmt.Sprintf("Unsuitable value for variable %q: %s.", i.DeclName, tfdiags.FormatError(err)),
|
||||
Subject: maybeHCLSourceRange(i.ValueSourceRange()),
|
||||
})
|
||||
finalV = cty.UnknownVal(i.TargetType.WithoutOptionalAttributesDeep())
|
||||
}
|
||||
|
||||
// TODO: Probably need to factor this part out into a separate function
|
||||
// so that we can collect up check results for inclusion in the checks
|
||||
// summary in the plan or state, but for now we're not worrying about
|
||||
// that because it's pretty rarely-used functionality.
|
||||
scopeBuilder := func(ctx context.Context, parent exprs.Scope) exprs.Scope {
|
||||
return &inputVariableValidationScope{
|
||||
wantName: i.DeclName,
|
||||
parentScope: parent,
|
||||
finalVal: finalV,
|
||||
}
|
||||
}
|
||||
for _, rule := range i.ValidationRules {
|
||||
status, moreDiags := rule.Check(ctx, scopeBuilder)
|
||||
diags = diags.Append(moreDiags)
|
||||
if status == checks.StatusFail {
|
||||
errMsg, moreDiags := rule.ErrorMessage(ctx, scopeBuilder)
|
||||
diags = diags.Append(moreDiags)
|
||||
if !moreDiags.HasErrors() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid value for input variable",
|
||||
Detail: fmt.Sprintf("%s\n\nThis problem was reported by the validation rule at %s.", errMsg, rule.DeclRange().StartString()),
|
||||
Subject: rule.ConditionRange().ToHCL().Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if diags.HasErrors() {
|
||||
// If we found any problems then we'll use an unknown result of the
|
||||
// expected type so that downstream expressions will only report
|
||||
// new problems and not consequences of the problems we already
|
||||
// reported.
|
||||
finalV = cty.UnknownVal(i.TargetType.WithoutOptionalAttributesDeep())
|
||||
}
|
||||
return finalV, diags
|
||||
}
|
||||
|
||||
// ValueSourceRange implements exprs.Valuer.
|
||||
func (i *InputVariable) ValueSourceRange() *tfdiags.SourceRange {
|
||||
return i.RawValue.ValueSourceRange()
|
||||
}
|
||||
|
||||
// inputVariableValidationScope is a specialized [exprs.Scope] implementation
|
||||
// that forces returning a constant value when accessing a specific input
|
||||
// variable directly, but otherwise just passes everything else through from
|
||||
// a parent scope.
|
||||
//
|
||||
// This is used for evaluating validation rules for an [InputVariable], where
|
||||
// we need to be able to evaluate an expression referring to the variable
|
||||
// as part of deciding the final value of the variable and so if we didn't
|
||||
// handle it directly then there would be a self-reference error.
|
||||
type inputVariableValidationScope struct {
|
||||
varTable exprs.SymbolTable
|
||||
wantName string
|
||||
parentScope exprs.Scope
|
||||
finalVal cty.Value
|
||||
}
|
||||
|
||||
var _ exprs.Scope = (*inputVariableValidationScope)(nil)
|
||||
var _ exprs.SymbolTable = (*inputVariableValidationScope)(nil)
|
||||
|
||||
// HandleInvalidStep implements exprs.Scope.
|
||||
func (i *inputVariableValidationScope) HandleInvalidStep(rng tfdiags.SourceRange) tfdiags.Diagnostics {
|
||||
return i.parentScope.HandleInvalidStep(rng)
|
||||
}
|
||||
|
||||
// ResolveAttr implements exprs.Scope.
|
||||
func (i *inputVariableValidationScope) ResolveAttr(ref hcl.TraverseAttr) (exprs.Attribute, tfdiags.Diagnostics) {
|
||||
if i.varTable == nil {
|
||||
// We're currently at the top-level scope where we're looking for
|
||||
// the "var." prefix to represent accessing any input variable at all.
|
||||
attr, diags := i.parentScope.ResolveAttr(ref)
|
||||
if diags.HasErrors() {
|
||||
return attr, diags
|
||||
}
|
||||
nestedTable := exprs.NestedSymbolTableFromAttribute(attr)
|
||||
if nestedTable != nil && ref.Name == "var" {
|
||||
// We'll return another instance of ourselves but with i.varTable
|
||||
// now populated to represent that the next step should try
|
||||
// to look up an input variable.
|
||||
return exprs.NestedSymbolTable(&inputVariableValidationScope{
|
||||
varTable: nestedTable,
|
||||
wantName: i.wantName,
|
||||
parentScope: i.parentScope,
|
||||
}), diags
|
||||
}
|
||||
// If it's anything other than the "var" prefix then we'll just return
|
||||
// whatever the parent scope returned directly, because we don't
|
||||
// need to be involved anymore.
|
||||
return attr, diags
|
||||
}
|
||||
|
||||
// If we get here then we're now nested under the "var." prefix, but
|
||||
// we only need to get involved if the reference is to the variable
|
||||
// we're currently validating.
|
||||
if ref.Name == i.wantName {
|
||||
return exprs.ValueOf(exprs.ConstantValuer(i.finalVal)), nil
|
||||
}
|
||||
return i.varTable.ResolveAttr(ref)
|
||||
}
|
||||
|
||||
// ResolveFunc implements exprs.Scope.
|
||||
func (i *inputVariableValidationScope) ResolveFunc(call *hcl.StaticCall) (function.Function, tfdiags.Diagnostics) {
|
||||
return i.parentScope.ResolveFunc(call)
|
||||
}
|
||||
40
internal/lang/eval/internal/configgraph/local_value.go
Normal file
40
internal/lang/eval/internal/configgraph/local_value.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// 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/lang/exprs"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
type LocalValue struct {
|
||||
RawValue exprs.Valuer
|
||||
}
|
||||
|
||||
var _ exprs.Valuer = (*LocalValue)(nil)
|
||||
|
||||
// StaticCheckTraversal implements exprs.Valuer.
|
||||
func (l *LocalValue) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics {
|
||||
return l.RawValue.StaticCheckTraversal(traversal)
|
||||
}
|
||||
|
||||
// Value implements exprs.Valuer.
|
||||
func (l *LocalValue) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
|
||||
// There aren't really any special rules for a local value: it just
|
||||
// allows authors to associate a value with a name so they can reuse
|
||||
// it multiple places.
|
||||
return l.RawValue.Value(ctx)
|
||||
}
|
||||
|
||||
// ValueSourceRange implements exprs.Valuer.
|
||||
func (l *LocalValue) ValueSourceRange() *tfdiags.SourceRange {
|
||||
return l.RawValue.ValueSourceRange()
|
||||
}
|
||||
311
internal/lang/eval/internal/configgraph/module_instance.go
Normal file
311
internal/lang/eval/internal/configgraph/module_instance.go
Normal file
@@ -0,0 +1,311 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package configgraph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/lang/exprs"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
type ModuleInstance struct {
|
||||
InputVariableNodes map[addrs.InputVariable]*InputVariable
|
||||
LocalValueNodes map[addrs.LocalValue]*LocalValue
|
||||
OutputValueNodes map[addrs.OutputValue]*OutputValue
|
||||
|
||||
CoreFunctions map[string]function.Function
|
||||
|
||||
// moduleSourceAddr is the source address of the module this is an
|
||||
// instance of, which will be used as the base address for resolving
|
||||
// any relative local source addresses in child calls.
|
||||
//
|
||||
// This must always be either [addrs.ModuleSourceLocal] or
|
||||
// [addrs.ModuleSourceRemote]. If the module was discovered indirectly
|
||||
// through an [addrs.ModuleSourceRegistry] then this records the
|
||||
// remote address that the registry address was resolved to, to ensure
|
||||
// that local source addresses will definitely resolve within exactly
|
||||
// the same remote package.
|
||||
ModuleSourceAddr addrs.ModuleSource
|
||||
|
||||
// callDeclRange is used for module instances that are produced because
|
||||
// of a "module" block in a parent module, or by some similar mechanism
|
||||
// like a .tftest.hcl "run" block, which can then be used as a source
|
||||
// range for the overall object value representing the module instance's
|
||||
// results.
|
||||
//
|
||||
// This is left as nil for module instances that are created implicitly,
|
||||
// such as a root module which is being "called" directly from OpenTofu CLI
|
||||
// in a command like "tofu plan".
|
||||
CallDeclRange *tfdiags.SourceRange
|
||||
}
|
||||
|
||||
var _ exprs.Valuer = (*ModuleInstance)(nil)
|
||||
var _ exprs.Scope = (*ModuleInstance)(nil)
|
||||
|
||||
// StaticCheckTraversal implements exprs.Valuer.
|
||||
func (m *ModuleInstance) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics {
|
||||
if len(traversal) == 0 {
|
||||
return nil // empty traversal is always valid
|
||||
}
|
||||
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
// The Value representation of a module instance is an object with an
|
||||
// attribute for each output value, and so the first step traverses
|
||||
// through that first level of attributes.
|
||||
outputName, ok := exprs.TraversalStepAttributeName(traversal[0])
|
||||
if !ok {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid reference to output value",
|
||||
Detail: "A module instance is represented by an object value whose attributes match the names of the output values declared inside the module.",
|
||||
Subject: traversal[0].SourceRange().Ptr(),
|
||||
})
|
||||
return diags
|
||||
}
|
||||
|
||||
output, ok := m.OutputValueNodes[addrs.OutputValue{Name: outputName}]
|
||||
if !ok {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Reference to undeclared output value",
|
||||
Detail: fmt.Sprintf("The child module does not declare any output value named %q.", outputName),
|
||||
Subject: traversal[0].SourceRange().Ptr(),
|
||||
})
|
||||
return diags
|
||||
}
|
||||
diags = diags.Append(
|
||||
exprs.StaticCheckTraversalThroughType(traversal[1:], output.ResultTypeConstraint()),
|
||||
)
|
||||
return diags
|
||||
}
|
||||
|
||||
// Value implements exprs.Valuer.
|
||||
func (m *ModuleInstance) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
|
||||
// The following is mechanically similar to evaluating an object constructor
|
||||
// expression gathering all of the output value results into a single
|
||||
// object, but because we're not using the expression evaluator to do it
|
||||
// we need to explicitly discard indirect diagnostics with
|
||||
// [diagsHandledElsewhere].
|
||||
attrs := make(map[string]cty.Value, len(m.OutputValueNodes))
|
||||
for addr, ov := range m.OutputValueNodes {
|
||||
attrs[addr.Name] = diagsHandledElsewhere(ov.Value(ctx))
|
||||
}
|
||||
return cty.ObjectVal(attrs), nil
|
||||
}
|
||||
|
||||
// ValueSourceRange implements exprs.Valuer.
|
||||
func (m *ModuleInstance) ValueSourceRange() *tfdiags.SourceRange {
|
||||
return m.CallDeclRange
|
||||
}
|
||||
|
||||
// HandleInvalidStep implements exprs.Scope.
|
||||
func (m *ModuleInstance) HandleInvalidStep(rng tfdiags.SourceRange) tfdiags.Diagnostics {
|
||||
// We can't actually get here in normal use because this is a top-level
|
||||
// scope and HCL only allows attribute-shaped access to top-level symbols,
|
||||
// which would be handled by [ModuleInstance.ResolveAttr] instead.
|
||||
//
|
||||
// This is here primarily for completeness/robustness, but should be
|
||||
// reachable only in the presence of weird hand-written [hcl.Traversal]
|
||||
// values that could not be produced by the HCL parsers.
|
||||
var diags tfdiags.Diagnostics
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid global reference",
|
||||
Detail: "Only static access to predeclared names is allowed in this scope.",
|
||||
Subject: rng.ToHCL().Ptr(),
|
||||
})
|
||||
return diags
|
||||
}
|
||||
|
||||
// ResolveAttr implements exprs.Scope.
|
||||
func (m *ModuleInstance) ResolveAttr(ref hcl.TraverseAttr) (exprs.Attribute, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
switch ref.Name {
|
||||
|
||||
case "var", "local", "module":
|
||||
// For various relatively-simple cases where there's just one-level of
|
||||
// nested symbol table we use a single shared [exprs.SymbolTable]
|
||||
// implementation which then just delegates back to
|
||||
// [ModuleInstance.resolveSimpleChildAttr] once it has collected the
|
||||
// nested symbol name. Refer to that function for more details on these.
|
||||
return exprs.NestedSymbolTable(&moduleInstNestedSymbolTable{topSymbol: ref.Name, moduleInst: m}), diags
|
||||
|
||||
case "each", "count", "self":
|
||||
// These symbols are not included in a module instance's global symbol
|
||||
// table at all, but we treat them as special here just so we can
|
||||
// return a different error message that implies that they are valid
|
||||
// in some other contexts even though they aren't valid here.
|
||||
//
|
||||
// Situations where these symbols _are_ available should be handled
|
||||
// by creating another [Scope] implementation which wraps this one,
|
||||
// handling these local symbols itself while delegating everything
|
||||
// else to [ModuleInstance.ResolveAttr] for handling as normal.
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Reference to unavailable local symbol",
|
||||
Detail: fmt.Sprintf("The symbol %q is not available in this location. It is available only locally in certain special parts of the language.", ref.Name),
|
||||
Subject: &ref.SrcRange,
|
||||
})
|
||||
return nil, diags
|
||||
|
||||
default:
|
||||
// TODO: Once we support resource references this case should be treated
|
||||
// as the beginning of a reference to a managed resource, as a
|
||||
// shorthand omitting the "resource." prefix.
|
||||
diags = diags.Append(fmt.Errorf("no support for %q references yet", ref.Name))
|
||||
return nil, diags
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModuleInstance) resolveSimpleChildAttr(topSymbol string, ref hcl.TraverseAttr) (exprs.Attribute, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
// NOTE: This function only handles top-level symbol names which are
|
||||
// delegated to [moduleInstNestedSymbolTable] by
|
||||
// [ModuleInstance.ResolveAttr]. Some top-level symbol names are handled
|
||||
// separately and so intentionally not included in the following.
|
||||
switch topSymbol {
|
||||
|
||||
case "var":
|
||||
v, ok := m.InputVariableNodes[addrs.InputVariable{Name: ref.Name}]
|
||||
if !ok {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Reference to undeclared input variable",
|
||||
Detail: fmt.Sprintf("There is no input variable named %q declared in this module.", ref.Name),
|
||||
Subject: &ref.SrcRange,
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
return exprs.ValueOf(v), diags
|
||||
|
||||
case "local":
|
||||
v, ok := m.LocalValueNodes[addrs.LocalValue{Name: ref.Name}]
|
||||
if !ok {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Reference to undeclared local value",
|
||||
Detail: fmt.Sprintf("There is no local value named %q declared in this module.", ref.Name),
|
||||
Subject: &ref.SrcRange,
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
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
|
||||
|
||||
default:
|
||||
// We should not get here because there should be a case above for
|
||||
// every symbol name that [ModuleInstance.ResolveAttr] delegates
|
||||
// to [moduleInstNestedSymbolTable].
|
||||
panic(fmt.Sprintf("missing handler for top-level symbol %q", topSymbol))
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveFunc implements exprs.Scope.
|
||||
func (m *ModuleInstance) ResolveFunc(call *hcl.StaticCall) (function.Function, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
if strings.Contains(call.Name, "::") {
|
||||
// TODO: Implement provider-defined functions, which use the
|
||||
// "provider::" prefix.
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Call to unsupported function",
|
||||
Detail: "This new experimental codepath doesn't support non-core functions yet.",
|
||||
Subject: &call.NameRange,
|
||||
})
|
||||
return function.Function{}, diags
|
||||
}
|
||||
|
||||
fn, ok := m.CoreFunctions[call.Name]
|
||||
if !ok {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Call to unsupported function",
|
||||
Detail: fmt.Sprintf("There is no core function named %q in this version of OpenTofu.", call.Name),
|
||||
Subject: &call.NameRange,
|
||||
})
|
||||
return function.Function{}, diags
|
||||
}
|
||||
|
||||
return fn, diags
|
||||
}
|
||||
|
||||
// moduleInstNestedSymbolTable is a common implementation for all of the
|
||||
// various "simple" nested symbol table prefixes in a module instance's
|
||||
// top-level scope, handling the typical case where there's a fixed prefix
|
||||
// symbol followed by a single child symbol, as in "var.foo".
|
||||
//
|
||||
// This does not handle more complicated cases like resource references
|
||||
// where there are multiple levels of nesting. Refer to
|
||||
// [ModuleInstance.ResolveAttr] to learn how each of the top-level symbols
|
||||
// is handled, and what subset of them are handled by this type.
|
||||
type moduleInstNestedSymbolTable struct {
|
||||
topSymbol string
|
||||
moduleInst *ModuleInstance
|
||||
}
|
||||
|
||||
var _ exprs.SymbolTable = (*moduleInstNestedSymbolTable)(nil)
|
||||
|
||||
// HandleInvalidStep implements exprs.SymbolTable.
|
||||
func (m *moduleInstNestedSymbolTable) HandleInvalidStep(rng tfdiags.SourceRange) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
noun := nounForModuleGlobalSymbol(m.topSymbol)
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid reference to " + noun,
|
||||
Detail: fmt.Sprintf("Reference to %s requires an attribute name.", noun),
|
||||
Subject: rng.ToHCL().Ptr(),
|
||||
})
|
||||
return diags
|
||||
}
|
||||
|
||||
// ResolveAttr implements exprs.SymbolTable.
|
||||
func (m *moduleInstNestedSymbolTable) ResolveAttr(ref hcl.TraverseAttr) (exprs.Attribute, tfdiags.Diagnostics) {
|
||||
// Now we just delegate back to the original module instance, so that
|
||||
// we can keep all of the symbol-table-related code relatively close
|
||||
// together.
|
||||
return m.moduleInst.resolveSimpleChildAttr(m.topSymbol, ref)
|
||||
}
|
||||
|
||||
func nounForModuleGlobalSymbol(symbol string) string {
|
||||
// This is a kinda-gross way to handle this. For example, it means that
|
||||
// callers generating error messages must use awkward grammar to avoid
|
||||
// dealing with "an input variable" vs "a local value".
|
||||
//
|
||||
// Can we find a better way while still reusing at least some code
|
||||
// between all of these relatively-simple symbol tables? Maybe it's
|
||||
// worth treating at least a few more of these as special just to
|
||||
// get some better error messages for the more common situations.
|
||||
switch symbol {
|
||||
case "var":
|
||||
return "input variable"
|
||||
case "local":
|
||||
return "local value"
|
||||
case "module":
|
||||
return "module call"
|
||||
default:
|
||||
return "attribute" // generic fallback that we should avoid using by adding new names above as needed
|
||||
}
|
||||
}
|
||||
54
internal/lang/eval/internal/configgraph/once_valuer.go
Normal file
54
internal/lang/eval/internal/configgraph/once_valuer.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// 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/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/lang/exprs"
|
||||
"github.com/opentofu/opentofu/internal/lang/grapheval"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
// OnceValuer wraps the given [exprs.Valuer] so that the underlying [Value]
|
||||
// method will be called only once and reused for all future calls.
|
||||
//
|
||||
// Calls to Value on the result must be made with a context derived from
|
||||
// one produced by [grapheval.ContextWithWorker], which is then used to
|
||||
// track and report dependency cycles. If the given context is not so
|
||||
// annotated then Value will immediately panic.
|
||||
//
|
||||
// The StaticCheckTraversal method is _not_ wrapped and so should be a
|
||||
// relatively cheap operation as usual and must not interact (directly or
|
||||
// indirectly) with any grapheval helpers.
|
||||
func OnceValuer(valuer exprs.Valuer) exprs.Valuer {
|
||||
return &onceValuer{inner: valuer}
|
||||
}
|
||||
|
||||
type onceValuer struct {
|
||||
once grapheval.Once[cty.Value]
|
||||
inner exprs.Valuer
|
||||
}
|
||||
|
||||
// StaticCheckTraversal implements exprs.Valuer.
|
||||
func (v *onceValuer) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics {
|
||||
return v.inner.StaticCheckTraversal(traversal)
|
||||
}
|
||||
|
||||
// Value implements exprs.Valuer.
|
||||
func (v *onceValuer) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
|
||||
return v.once.Do(ctx, func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
|
||||
return v.inner.Value(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
// ValueSourceRange implements exprs.Valuer.
|
||||
func (v *onceValuer) ValueSourceRange() *tfdiags.SourceRange {
|
||||
return v.inner.ValueSourceRange()
|
||||
}
|
||||
142
internal/lang/eval/internal/configgraph/output_value.go
Normal file
142
internal/lang/eval/internal/configgraph/output_value.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package configgraph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/ext/typeexpr"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/checks"
|
||||
"github.com/opentofu/opentofu/internal/lang/exprs"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
type OutputValue struct {
|
||||
// DeclName is the name of the variable as written in the header of its
|
||||
// declaration block.
|
||||
DeclName string
|
||||
|
||||
// Preconditions are user-defined checks that must succeed before OpenTofu
|
||||
// will evaluate the output value's expression.
|
||||
Preconditions []CheckRule
|
||||
|
||||
// RawValue produces the "raw" value, as chosen by the caller of the
|
||||
// module, which has not yet been type-converted or validated.
|
||||
RawValue exprs.Valuer
|
||||
|
||||
// TargetType and targetDefaults together represent the type conversion
|
||||
// and default object attribute value insertions that must be applied
|
||||
// to rawValue to produce the final result.
|
||||
TargetType cty.Type
|
||||
TargetDefaults *typeexpr.Defaults
|
||||
|
||||
// If ForceSensitive is true then the final value will be marked as
|
||||
// sensitive regardless of whether the associated raw value was sensitive.
|
||||
ForceSensitive bool
|
||||
|
||||
// If ForceEphemeral is true then the final value will be marked as
|
||||
// ephemeral regardless of whether the associated raw value was ephemeral.
|
||||
ForceEphemeral bool
|
||||
}
|
||||
|
||||
var _ exprs.Valuer = (*OutputValue)(nil)
|
||||
|
||||
// ResultTypeConstraint returns a type constraint that all possible results
|
||||
// of this output value are guaranteed to conform to.
|
||||
//
|
||||
// The result is [cty.DynamicPseudoType] for an output value which has no
|
||||
// declared type constraint, meaning that there is no guarantee whatsoever
|
||||
// about the result type.
|
||||
func (o *OutputValue) ResultTypeConstraint() cty.Type {
|
||||
return o.TargetType.WithoutOptionalAttributesDeep()
|
||||
}
|
||||
|
||||
// StaticCheckTraversal implements exprs.Valuer.
|
||||
func (o *OutputValue) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics {
|
||||
// We're checking against the type constraint that the final value is
|
||||
// guaranteed to conform to, rather than whatever type the raw value
|
||||
// has, because conversion to a target type with optional attributes
|
||||
// can potentially introduce new attributes. However, we need to
|
||||
// discard the optional attribute information first because
|
||||
// exprs.StaticCheckTraversalThroughType wants a type constraint, not
|
||||
// a "target type" for type conversion.
|
||||
typeConstraint := o.TargetType.WithoutOptionalAttributesDeep()
|
||||
return exprs.StaticCheckTraversalThroughType(traversal, typeConstraint)
|
||||
}
|
||||
|
||||
// Value implements exprs.Valuer.
|
||||
func (o *OutputValue) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
// The preconditions "guard" the evaluation of the output value's
|
||||
// expression, so we need to check them first and skip trying to evaluate
|
||||
// if any of them fail. This allows module authors to use preconditions
|
||||
// to provide a more specialized error message for certain cases, which
|
||||
// would then replace a more general error message that might otherwise
|
||||
// be produced by expression evaluation.
|
||||
//
|
||||
// TODO: Probably need to factor this part out into a separate function
|
||||
// so that we can collect up check results for inclusion in the checks
|
||||
// summary in the plan or state, but for now we're not worrying about
|
||||
// that because it's pretty rarely-used functionality.
|
||||
for _, rule := range o.Preconditions {
|
||||
status, moreDiags := rule.Check(ctx, nil)
|
||||
diags = diags.Append(moreDiags)
|
||||
if status == checks.StatusFail {
|
||||
errMsg, moreDiags := rule.ErrorMessage(ctx, nil)
|
||||
diags = diags.Append(moreDiags)
|
||||
if !moreDiags.HasErrors() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Output value precondition failed",
|
||||
Detail: fmt.Sprintf("%s\n\nThis problem was reported by the precondition at %s.", errMsg, rule.DeclRange().StartString()),
|
||||
Subject: rule.ConditionRange().ToHCL().Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if diags.HasErrors() {
|
||||
// If the preconditions caused at least one error then we must
|
||||
// not proceed any further.
|
||||
return cty.UnknownVal(o.TargetType.WithoutOptionalAttributesDeep()), diags
|
||||
}
|
||||
|
||||
rawV, diags := o.RawValue.Value(ctx)
|
||||
if o.TargetDefaults != nil {
|
||||
rawV = o.TargetDefaults.Apply(rawV)
|
||||
}
|
||||
finalV, err := convert.Convert(rawV, o.TargetType)
|
||||
if err != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid value for output value",
|
||||
Detail: fmt.Sprintf("Unsuitable value for output value %q: %s.", o.DeclName, tfdiags.FormatError(err)),
|
||||
Subject: maybeHCLSourceRange(o.ValueSourceRange()),
|
||||
})
|
||||
finalV = cty.UnknownVal(o.TargetType.WithoutOptionalAttributesDeep())
|
||||
}
|
||||
|
||||
if o.ForceSensitive {
|
||||
finalV = finalV.Mark(marks.Sensitive)
|
||||
}
|
||||
if o.ForceEphemeral {
|
||||
finalV = finalV.Mark(marks.Ephemeral)
|
||||
}
|
||||
// TODO: deprecation marks
|
||||
|
||||
return finalV, diags
|
||||
}
|
||||
|
||||
// ValueSourceRange implements exprs.Valuer.
|
||||
func (o *OutputValue) ValueSourceRange() *tfdiags.SourceRange {
|
||||
return o.RawValue.ValueSourceRange()
|
||||
}
|
||||
220
internal/lang/eval/module_instance.go
Normal file
220
internal/lang/eval/module_instance.go
Normal file
@@ -0,0 +1,220 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package eval
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/configs"
|
||||
"github.com/opentofu/opentofu/internal/lang/eval/internal/configgraph"
|
||||
"github.com/opentofu/opentofu/internal/lang/exprs"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
type moduleInstanceCall struct {
|
||||
// declRange is the source location of the header of the module block
|
||||
// that is making this call, or some similar config construct that's
|
||||
// acting like a module call.
|
||||
//
|
||||
// This should be nil for calls that are caused by something other than
|
||||
// configuration, such as a top-level call to a root module caused by
|
||||
// running an OpenTofu CLI command.
|
||||
declRange *tfdiags.SourceRange
|
||||
|
||||
// inputValues describes how to build the values for the input variables
|
||||
// for this instance of the module.
|
||||
//
|
||||
// For a call caused by a "module" block in a parent module, these would
|
||||
// be closures binding the expressions written in the module block to
|
||||
// the scope of the module block. The scope of the module block should
|
||||
// include the each.key/each.value/count.index symbols initialized as
|
||||
// appropriate for this specific instance of the module call. It's
|
||||
// the caller of [compileModuleInstance]'s responsibility to set these
|
||||
// up correctly so that the child module can be compiled with no direct
|
||||
// awareness of where it's being called from.
|
||||
inputValues map[addrs.InputVariable]exprs.Valuer
|
||||
|
||||
// TODO: provider instances from the "providers" argument in the
|
||||
// calling module, once we have enough of this implemented for that
|
||||
// to be useful. Will need to straighten out the address types
|
||||
// for provider configs and instances in package addrs first so
|
||||
// that we finally have a proper address type for a provider
|
||||
// instance with an instance key.
|
||||
}
|
||||
|
||||
// compileModuleInstance is the main entry point for binding a module
|
||||
// configuration to information from an instance of a module call and
|
||||
// producing a [configgraph.ModuleInstance] representation of the
|
||||
// resulting module instance, ready for continued evaluation.
|
||||
//
|
||||
// For those coming to this familiar with the previous language runtime
|
||||
// implementation in "package tofu": this is _roughly_ analogous to the
|
||||
// graph building process but is focused only on the configuration of
|
||||
// a single module (no state, no other modules) and is written as much
|
||||
// as possible as straightforward linear code, with inversion of control
|
||||
// techniques only where it's useful to separate concerns.
|
||||
func compileModuleInstance(
|
||||
module *configs.Module,
|
||||
|
||||
// FIXME: This is a separate argument for now because in current
|
||||
// "package configs" this is treated as a property of the [configs.Config]
|
||||
// instead of the [configs.Module], but we're intentionally not using
|
||||
// [configs.Config] here because this new design assembles the module tree
|
||||
// gradually during evaluation rather than up front during loading.
|
||||
//
|
||||
// If we decide to take this direction we should track the source
|
||||
// address as a field of [configs.Module] so that we don't need this
|
||||
// extra argument.
|
||||
moduleSourceAddr addrs.ModuleSource,
|
||||
|
||||
call *moduleInstanceCall,
|
||||
evalCtx *EvalContext,
|
||||
) *configgraph.ModuleInstance {
|
||||
// -----------------------------------------------------------------------
|
||||
// This intentionally has no direct error return path, because:
|
||||
// 1. The code that builds *configs.Module should already have reported
|
||||
// any "static" problems like syntax errors and hard structural
|
||||
// problems and thus prevented us from even reaching this call if
|
||||
// any were present.
|
||||
// 2. This "compiling" step is mainly about wiring things together in
|
||||
// preparation for evaluation rather than actually evaluating, and so
|
||||
// _dynamic_ problems will be handled during the subsequent evaluation
|
||||
// step rather than during this compilation process.
|
||||
//
|
||||
// If the work performed by this function _does_ discover something that's
|
||||
// invalid enough that it's impossible to construct valid evaluation
|
||||
// objects, then use mechanisms like [exprs.ForceErrorValuer] to arrange
|
||||
// for predefined error diagnostics to be discovered during evaluation
|
||||
// instead of returning them directly from here.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// We'll build this object up gradually because what we're ultimately going
|
||||
// to return is an implied graph of the relationships between everything
|
||||
// declared in this module, represented either directly by pointers or
|
||||
// indirectly through expressions, and so for the remainder of this
|
||||
// function we need to be careful in how we interact with the methods of
|
||||
// [configgraph.ModuleInstance] since many of them only make sense to
|
||||
// call everything has been completely assembled.
|
||||
ret := &configgraph.ModuleInstance{
|
||||
ModuleSourceAddr: moduleSourceAddr,
|
||||
CallDeclRange: call.declRange,
|
||||
}
|
||||
ret.InputVariableNodes = compileModuleInstanceInputVariables(module.Variables, call.inputValues, ret, call.declRange)
|
||||
ret.LocalValueNodes = compileModuleInstanceLocalValues(module.Locals, ret)
|
||||
ret.OutputValueNodes = compileModuleInstanceOutputValues(module.Outputs, ret)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func compileModuleInstanceInputVariables(configs map[string]*configs.Variable, values map[addrs.InputVariable]exprs.Valuer, declScope exprs.Scope, missingDefRange *tfdiags.SourceRange) map[addrs.InputVariable]*configgraph.InputVariable {
|
||||
ret := make(map[addrs.InputVariable]*configgraph.InputVariable, len(configs))
|
||||
for name, vc := range configs {
|
||||
addr := addrs.InputVariable{Name: name}
|
||||
|
||||
rawValue, ok := values[addr]
|
||||
if !ok {
|
||||
diagRange := vc.DeclRange
|
||||
if missingDefRange != nil {
|
||||
// better to blame the definition site than the declaration
|
||||
// site if we have enough information to do that.
|
||||
diagRange = missingDefRange.ToHCL()
|
||||
}
|
||||
if vc.Required() {
|
||||
// We don't actually _need_ to handle an error here because
|
||||
// the final evaluation of the variables must deal with the
|
||||
// possibility of the final value being null anyway, but
|
||||
// by handling this here we can produce a more helpful error
|
||||
// message that talks about the definition being statically
|
||||
// absent instead of dynamically null.
|
||||
var diags tfdiags.Diagnostics
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Missing definition for required input variable",
|
||||
Detail: fmt.Sprintf("Input variable %q is required, and so it must be provided as an argument to this module.", name),
|
||||
Subject: &diagRange,
|
||||
})
|
||||
rawValue = exprs.ForcedErrorValuer(diags)
|
||||
} else {
|
||||
// For a non-required variable we'll provide a placeholder
|
||||
// null value so that the evaluator can treat this the same
|
||||
// as if there was an explicit definition evaluating to null.
|
||||
rawValue = exprs.ConstantValuerWithSourceRange(
|
||||
cty.NullVal(vc.Type),
|
||||
tfdiags.SourceRangeFromHCL(diagRange),
|
||||
)
|
||||
}
|
||||
}
|
||||
ret[addr] = &configgraph.InputVariable{
|
||||
DeclName: name,
|
||||
RawValue: configgraph.OnceValuer(rawValue),
|
||||
TargetType: vc.ConstraintType,
|
||||
TargetDefaults: vc.TypeDefaults,
|
||||
ValidationRules: compileCheckRules(vc.Validations, declScope, vc.Ephemeral),
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func compileModuleInstanceLocalValues(configs map[string]*configs.Local, declScope exprs.Scope) map[addrs.LocalValue]*configgraph.LocalValue {
|
||||
ret := make(map[addrs.LocalValue]*configgraph.LocalValue, len(configs))
|
||||
for name, vc := range configs {
|
||||
addr := addrs.LocalValue{Name: name}
|
||||
value := configgraph.OnceValuer(exprs.NewClosure(
|
||||
exprs.EvalableHCLExpression(vc.Expr),
|
||||
declScope,
|
||||
))
|
||||
ret[addr] = &configgraph.LocalValue{
|
||||
RawValue: value,
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func compileModuleInstanceOutputValues(configs map[string]*configs.Output, declScope *configgraph.ModuleInstance) map[addrs.OutputValue]*configgraph.OutputValue {
|
||||
ret := make(map[addrs.OutputValue]*configgraph.OutputValue, len(configs))
|
||||
for name, vc := range configs {
|
||||
addr := addrs.OutputValue{Name: name}
|
||||
value := configgraph.OnceValuer(exprs.NewClosure(
|
||||
exprs.EvalableHCLExpression(vc.Expr),
|
||||
declScope,
|
||||
))
|
||||
ret[addr] = &configgraph.OutputValue{
|
||||
DeclName: name,
|
||||
RawValue: value,
|
||||
|
||||
// Our current language doesn't allow specifying a type constraint
|
||||
// for an output value, so these are always the most liberal
|
||||
// possible constraint. Making these customizable could be part
|
||||
// of a solution to:
|
||||
// https://github.com/opentofu/opentofu/issues/2831
|
||||
TargetType: cty.DynamicPseudoType,
|
||||
TargetDefaults: nil,
|
||||
|
||||
ForceSensitive: vc.Sensitive,
|
||||
ForceEphemeral: vc.Ephemeral,
|
||||
Preconditions: compileCheckRules(vc.Preconditions, declScope, vc.Ephemeral),
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func compileCheckRules(configs []*configs.CheckRule, declScope exprs.Scope, ephemeralAllowed bool) []configgraph.CheckRule {
|
||||
ret := make([]configgraph.CheckRule, 0, len(configs))
|
||||
for _, config := range configs {
|
||||
ret = append(ret, configgraph.CheckRule{
|
||||
Condition: exprs.EvalableHCLExpression(config.Condition),
|
||||
ErrorMessageRaw: exprs.EvalableHCLExpression(config.ErrorMessage),
|
||||
ParentScope: declScope,
|
||||
EphemeralAllowed: ephemeralAllowed,
|
||||
DeclSourceRange: tfdiags.SourceRangeFromHCL(config.DeclRange),
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
56
internal/lang/eval/module_instance_test.go
Normal file
56
internal/lang/eval/module_instance_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package eval
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/zclconf/go-cty-debug/ctydebug"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/configs"
|
||||
"github.com/opentofu/opentofu/internal/lang/grapheval"
|
||||
)
|
||||
|
||||
func TestCompileModuleInstance_valuesOnly(t *testing.T) {
|
||||
// This is a relatively straightforward test of compiling a module
|
||||
// instance containing only inert values (no resources, etc) and
|
||||
// then pulling values out of it to make sure that it's wired together
|
||||
// correctly. This is far from exhaustive but covers some of
|
||||
// the fundamentals that more complex situations rely on.
|
||||
|
||||
module := configs.ModuleFromStringForTesting(t, `
|
||||
variable "a" {
|
||||
type = string
|
||||
}
|
||||
locals {
|
||||
b = "${var.a}:${var.a}"
|
||||
}
|
||||
output "c" {
|
||||
value = "${local.b}/${local.b}"
|
||||
}
|
||||
`)
|
||||
call := &moduleInstanceCall{
|
||||
inputValues: InputValuesForTesting(map[string]cty.Value{
|
||||
"a": cty.True,
|
||||
}),
|
||||
}
|
||||
inst := compileModuleInstance(module, addrs.ModuleSourceLocal("."), call, nil)
|
||||
|
||||
ctx := grapheval.ContextWithNewWorker(t.Context())
|
||||
got, diags := inst.Value(ctx)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors: %s", diags.Err().Error())
|
||||
}
|
||||
want := cty.ObjectVal(map[string]cty.Value{
|
||||
"c": cty.StringVal("true:true/true:true"),
|
||||
})
|
||||
if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" {
|
||||
t.Error("wrong result\n" + diff)
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,17 @@ func ValueOf(v Valuer) Attribute {
|
||||
return valueOf{v}
|
||||
}
|
||||
|
||||
// NestedSymbolTableFromAttribute returns the symbol table from an attribute
|
||||
// that was returned from [NestedSymbolTable], or nil for any other kind of
|
||||
// attribute.
|
||||
func NestedSymbolTableFromAttribute(attr Attribute) SymbolTable {
|
||||
withTable, ok := attr.(nestedSymbolTable)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return withTable.SymbolTable
|
||||
}
|
||||
|
||||
// nestedSymbolTable is the [Attribute] implementation for symbols that act as
|
||||
// nested symbol tables, resolving another set of child attributes within.
|
||||
type nestedSymbolTable struct {
|
||||
|
||||
@@ -29,6 +29,13 @@ var _ Valuer = (*Closure)(nil)
|
||||
// NewClosure associates the given [Evalable] with the given [Scope] so that
|
||||
// it can be evaluated somewhere else later without losing track of what symbols
|
||||
// and functions were available where it was declared.
|
||||
//
|
||||
// Passing a nil Scope is valid, and represents that there are absolutely no
|
||||
// symbols or functions available for use in the given Evalable. Note that HCL's
|
||||
// JSON syntax treats that situation quite differently by taking JSON strings
|
||||
// totally literally instead of trying to interpret them as HCL templates, and
|
||||
// so switching to or from a nil scope is typically a breaking change for what's
|
||||
// allowed in a particular position.
|
||||
func NewClosure(evalable Evalable, scope Scope) *Closure {
|
||||
return &Closure{evalable, scope}
|
||||
}
|
||||
@@ -49,3 +56,9 @@ func (c *Closure) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnost
|
||||
func (c *Closure) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
|
||||
return Evaluate(ctx, c.evalable, c.scope)
|
||||
}
|
||||
|
||||
// SourceRange returns the source range of the underlying [Evalable].
|
||||
func (c *Closure) ValueSourceRange() *tfdiags.SourceRange {
|
||||
ret := c.evalable.EvalableSourceRange()
|
||||
return &ret
|
||||
}
|
||||
|
||||
@@ -50,6 +50,13 @@ type Evalable interface {
|
||||
// may internally block on the completion of a potentially-time-consuming
|
||||
// operation, in which case they should respond gracefully to the
|
||||
// cancellation or deadline of the given context.
|
||||
//
|
||||
// If Evaluate returns diagnostics then it must also return a suitable
|
||||
// placeholder value that could be use for downstream expression evaluation
|
||||
// despite the error. Returning [cty.DynamicVal] is acceptable if all else
|
||||
// fails, but returning an unknown value with a more specific type
|
||||
// constraint can give more opportunities to proactively detect downstream
|
||||
// errors in a single evaluation pass.
|
||||
Evaluate(ctx context.Context, hclCtx *hcl.EvalContext) (cty.Value, tfdiags.Diagnostics)
|
||||
|
||||
// ResultTypeConstraint returns a type constrant that all possible results
|
||||
@@ -63,6 +70,10 @@ type Evalable interface {
|
||||
// knew the types of everything that'd be passed in hclCtx when calling
|
||||
// Evaluate. Is there some way we can approximate that?
|
||||
ResultTypeConstraint() cty.Type
|
||||
|
||||
// EvalableSourceRange returns a description of a source location that this
|
||||
// Evalable was derived from.
|
||||
EvalableSourceRange() tfdiags.SourceRange
|
||||
}
|
||||
|
||||
func StaticCheckTraversal(traversal hcl.Traversal, evalable Evalable) tfdiags.Diagnostics {
|
||||
@@ -97,6 +108,9 @@ func (h hclExpression) Evaluate(ctx context.Context, hclCtx *hcl.EvalContext) (c
|
||||
var diags tfdiags.Diagnostics
|
||||
v, hclDiags := h.expr.Value(hclCtx)
|
||||
diags = diags.Append(hclDiags)
|
||||
if hclDiags.HasErrors() {
|
||||
v = cty.UnknownVal(h.ResultTypeConstraint()).WithSameMarks(v)
|
||||
}
|
||||
return v, diags
|
||||
}
|
||||
|
||||
@@ -125,6 +139,11 @@ func (h hclExpression) ResultTypeConstraint() cty.Type {
|
||||
return v.Type()
|
||||
}
|
||||
|
||||
// SourceRange implements Evalable.
|
||||
func (h hclExpression) EvalableSourceRange() tfdiags.SourceRange {
|
||||
return tfdiags.SourceRangeFromHCL(h.expr.Range())
|
||||
}
|
||||
|
||||
// hclBody implements [Evalable] for a [hcl.Body] and associated [hcldec.Spec].
|
||||
type hclBody struct {
|
||||
body hcl.Body
|
||||
@@ -142,6 +161,9 @@ func (h *hclBody) Evaluate(ctx context.Context, hclCtx *hcl.EvalContext) (cty.Va
|
||||
var diags tfdiags.Diagnostics
|
||||
v, hclDiags := hcldec.Decode(h.body, h.spec, hclCtx)
|
||||
diags = diags.Append(hclDiags)
|
||||
if hclDiags.HasErrors() {
|
||||
v = cty.UnknownVal(h.ResultTypeConstraint()).WithSameMarks(v)
|
||||
}
|
||||
return v, diags
|
||||
}
|
||||
|
||||
@@ -161,3 +183,12 @@ func (h *hclBody) References() iter.Seq[hcl.Traversal] {
|
||||
func (h *hclBody) ResultTypeConstraint() cty.Type {
|
||||
return hcldec.ImpliedType(h.spec)
|
||||
}
|
||||
|
||||
// SourceRange implements Evalable.
|
||||
func (h *hclBody) EvalableSourceRange() tfdiags.SourceRange {
|
||||
// The "missing item range" is not necessarily a good range to use here,
|
||||
// but is the best we can do. At least in HCL native syntax this tends
|
||||
// to be in the header of the block that contained the body and so
|
||||
// is _close_ to the body being described.
|
||||
return tfdiags.SourceRangeFromHCL(h.body.MissingItemRange())
|
||||
}
|
||||
|
||||
@@ -23,19 +23,36 @@ import (
|
||||
// Some [Evaluable] implementations (or the symbols they refer to) can block
|
||||
// on potentially-time-consuming operations, in which case they should respond
|
||||
// gracefully to cancellation of the given context.
|
||||
//
|
||||
// It's valid to pass a nil Scope, representing that no symbols or functions
|
||||
// are available at all. Note that HCL's JSON syntax treats that situation
|
||||
// quite differently by taking JSON strings totally literally instead of
|
||||
// trying to interpret them as HCL templates, and so switching to or from
|
||||
// a nil scope is typically a breaking change for what's allowed in a
|
||||
// particular position.
|
||||
func Evaluate(ctx context.Context, what Evalable, scope Scope) (cty.Value, tfdiags.Diagnostics) {
|
||||
hclCtx, diags := buildHCLEvalContext(ctx, what, scope)
|
||||
if diags.HasErrors() {
|
||||
return cty.DynamicVal, diags
|
||||
return cty.DynamicVal.Mark(EvalError), diags
|
||||
}
|
||||
val, moreDiags := what.Evaluate(ctx, hclCtx)
|
||||
diags = diags.Append(moreDiags)
|
||||
if diags.HasErrors() {
|
||||
val = val.Mark(EvalError)
|
||||
}
|
||||
return val, diags
|
||||
}
|
||||
|
||||
func buildHCLEvalContext(ctx context.Context, what Evalable, scope Scope) (*hcl.EvalContext, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
ret := &hcl.EvalContext{}
|
||||
if scope == nil {
|
||||
// A nil scope represents that nothing at all is available, which HCL
|
||||
// represents as an EvalContext with nothing defined inside it.
|
||||
// Note that this causes significantly different behavior for HCL's
|
||||
// JSON syntax.
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
symbols, moreDiags := buildSymbolTable(ctx, what.References(), scope)
|
||||
ret.Variables = symbols
|
||||
@@ -145,8 +162,17 @@ func valuesForSymbolTableTempNodes(ctx context.Context, symbols map[string]*symb
|
||||
continue
|
||||
}
|
||||
|
||||
val, moreDiags := node.val.Value(ctx)
|
||||
diags = diags.Append(moreDiags)
|
||||
// When we take the value of the object being referred to we
|
||||
// intentionally ignore any _indirect_ diagnostics that might
|
||||
// cause because we only want to report diagnostics that are
|
||||
// directly related to the expression we're currently
|
||||
// evaluating. Whatever problems might exist in the definition
|
||||
// of what we're referring to must be caught by visiting
|
||||
// that thing and evaluating it directly. This is safe to do
|
||||
// because the definition of Valuer requires that Value must
|
||||
// always return some reasonable placeholder value to use even
|
||||
// when an error occurs.
|
||||
val, _ := node.val.Value(ctx)
|
||||
ret[name] = val
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -263,6 +263,11 @@ func (t *testInputVariable) Value(ctx context.Context) (cty.Value, tfdiags.Diagn
|
||||
return v, diags
|
||||
}
|
||||
|
||||
// ValueSourceRange implements exprs.Valuer.
|
||||
func (t *testInputVariable) ValueSourceRange() *tfdiags.SourceRange {
|
||||
return &t.valRange
|
||||
}
|
||||
|
||||
// testInputVariables is an intermediate [SymbolTable] implementation dealing
|
||||
// with symbols under "resource.", "data.", and "ephemeral.".
|
||||
type testResourcesOfMode struct {
|
||||
@@ -378,6 +383,11 @@ func (t *testResource) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostic
|
||||
return t.config.Value(ctx)
|
||||
}
|
||||
|
||||
// ValueSourceRange implements exprs.Valuer.
|
||||
func (t *testResource) ValueSourceRange() *tfdiags.SourceRange {
|
||||
return t.config.ValueSourceRange()
|
||||
}
|
||||
|
||||
// exampleMustParseTfvars is a helper function just to make these contrived
|
||||
// examples a little more concise, which tries to interpret the given string
|
||||
// in a similar way to how OpenTofu would normally deal with a ".tfvars" file.
|
||||
|
||||
91
internal/lang/exprs/marks.go
Normal file
91
internal/lang/exprs/marks.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package exprs
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// evaluationMark is the type used for a few cty marks we use to help
|
||||
// distinguish normal unknown value results from those used as placeholders
|
||||
// for failed evaluation of upstream objects.
|
||||
type evalResultMark rune
|
||||
|
||||
// EvalError is a [cty.Value] mark used on placeholder unknown values returned
|
||||
// whenever evaluation causes error diagnostics.
|
||||
//
|
||||
// This is intended only for use between collaborators in a subsystem where
|
||||
// everyone is consistently following this convention as a means to avoid
|
||||
// redundantly reporting downstream consequences of an upstream problem.
|
||||
// Use [WithoutEvalErrorMarks] at the boundary of such a subsystem so that
|
||||
// code in other parts of the system does not need to deal with these marks.
|
||||
//
|
||||
// In many cases it's okay to ignore this mark and just use the unknown value
|
||||
// placeholder as normal, letting the mark "infect" the result as necessary,
|
||||
// but this is here for less common situations where logic _does_ need to handle
|
||||
// those situations differently.
|
||||
//
|
||||
// For example, if a particular language feature treats the mere presence of
|
||||
// an unknown value as an error then the error-handling logic should first
|
||||
// check whether the value has this mark and only return the
|
||||
// unknown-value-related error if not, because the presence of this mark
|
||||
// suggests that the unknown value is likely caused by another upstream error
|
||||
// rather than by the module author directly using an unknown value in an
|
||||
// invalid location.
|
||||
//
|
||||
// The expression evaluation mechanisms in this package add this mark
|
||||
// automatically whenever they generate evaluation error placeholders, but
|
||||
// it's exposed as an exported symbol so that logic elsewhere that is performing
|
||||
// non-expression-based evaluation can participate in this marking scheme.
|
||||
const EvalError = evalResultMark('E')
|
||||
|
||||
// IsEvalError returns true if the given value is directly marked with
|
||||
// [EvalError], indicating that it's acting as a placeholder for an upstream
|
||||
// failed evaluation.
|
||||
//
|
||||
// This only checks the given value shallowly. Use [HasEvalErrors] instead to
|
||||
// check whether there are any evaluation error placeholders in nested values.
|
||||
// For example, a caller that is using [`cty.Value.IsWhollyKnown`] to reject
|
||||
// a value with unknown values anywhere inside it should prefer to use
|
||||
// [HasEvalErrors] first to determine if any of the nested unknown values
|
||||
// might actually be error placeholders.
|
||||
func IsEvalError(v cty.Value) bool {
|
||||
return v.HasMark(EvalError)
|
||||
}
|
||||
|
||||
// HasEvalErrors is like [IsEvalError] except that it visits nested values
|
||||
// inside the given value recursively and returns true if [EvalError] marks
|
||||
// are present at any nesting level.
|
||||
//
|
||||
// Don't use this except when rejecting values that contain nested unknown
|
||||
// values in a context where those values are not allowed. If only _shallow_
|
||||
// unknown values are disallowed then use [IsEvalError] instead to match
|
||||
// that with only a shallow check for the [EvalError] mark.
|
||||
func HasEvalErrors(v cty.Value) bool {
|
||||
_, marks := v.UnmarkDeep()
|
||||
_, marked := marks[EvalError]
|
||||
return marked
|
||||
}
|
||||
|
||||
// WithoutEvalErrorMarks returns the given value with any shallow or nested
|
||||
// [EvalError] marks removed.
|
||||
//
|
||||
// Use this at the boundary of a subsystem that uses the evaluation error
|
||||
// marking scheme internally as an implementation detail, to avoid exposing
|
||||
// this extra complexity to callers that are merely consuming the finalized
|
||||
// results.
|
||||
func WithoutEvalErrorMarks(v cty.Value) cty.Value {
|
||||
unmarked, pathMarks := v.UnmarkDeepWithPaths()
|
||||
var filteredPathMarks []cty.PathValueMarks
|
||||
// Locate EvalError marks and filter them out
|
||||
for _, pm := range pathMarks {
|
||||
delete(pm.Marks, EvalError)
|
||||
if len(pm.Marks) > 0 {
|
||||
filteredPathMarks = append(filteredPathMarks, pm)
|
||||
}
|
||||
}
|
||||
return unmarked.MarkWithPaths(filteredPathMarks)
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
package exprs
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
@@ -29,3 +31,18 @@ type Scope interface {
|
||||
// implementation or error diagnostics if no such function exists.
|
||||
ResolveFunc(call *hcl.StaticCall) (function.Function, tfdiags.Diagnostics)
|
||||
}
|
||||
|
||||
// ChildScopeBuilder is the signature for a function that can build a child
|
||||
// scope that wraps some given parent scope.
|
||||
//
|
||||
// A nil [ChildScopeBuilder] represents that no child scope is needed and the
|
||||
// parent should just be used directly. Use [ChildScopeBuilder.Build] instead
|
||||
// of directly calling the function to obtain that behavior automatically.
|
||||
type ChildScopeBuilder func(ctx context.Context, parent Scope) Scope
|
||||
|
||||
func (b ChildScopeBuilder) Build(ctx context.Context, parent Scope) Scope {
|
||||
if b == nil {
|
||||
return parent
|
||||
}
|
||||
return b(ctx, parent)
|
||||
}
|
||||
|
||||
51
internal/lang/exprs/traversals.go
Normal file
51
internal/lang/exprs/traversals.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package exprs
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
)
|
||||
|
||||
// TraversalStepAttributeName attempts to interpret the given traversal step
|
||||
// in a manner compatible with how [hcl.Index] would apply it to a value
|
||||
// of an object type, returning the name of the attribute it would access.
|
||||
//
|
||||
// If the second return value is false then the given step is not valid to
|
||||
// use in that situation.
|
||||
//
|
||||
// This is mainly for use in [Valuer.StaticCheckTraversal] implementations
|
||||
// where the Value method would return an object type, to find out which
|
||||
// attribute name the first traversal step would ultimately select.
|
||||
func TraversalStepAttributeName(step hcl.Traverser) (string, bool) {
|
||||
switch step := step.(type) {
|
||||
case hcl.TraverseAttr:
|
||||
return step.Name, true
|
||||
case hcl.TraverseRoot:
|
||||
return step.Name, true
|
||||
case hcl.TraverseIndex:
|
||||
v, err := convert.Convert(step.Key, cty.String)
|
||||
if err != nil || v.IsNull() {
|
||||
return "", false
|
||||
}
|
||||
if !v.IsKnown() {
|
||||
// Unknown values should not typically appear in traversals
|
||||
// because HCL itself only builds traversals from static
|
||||
// traversal syntax, but this is here just in case we do
|
||||
// something weird e.g. in a test that's constructing
|
||||
// traversals by hand.
|
||||
return "", false
|
||||
}
|
||||
// Likewise marked values should not typically appear in traversals
|
||||
// for the same reason, so we're unmarking for robustness against
|
||||
// weird contrived inputs only.
|
||||
v, _ = v.Unmark()
|
||||
return v.AsString(), true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,13 @@ type Valuer interface {
|
||||
// may internally block on the completion of a potentially-time-consuming
|
||||
// operation, in which case they should respond gracefully to the
|
||||
// cancellation or deadline of the given context.
|
||||
//
|
||||
// If Value returns diagnostics then it must also return a suitable
|
||||
// placeholder value that could be use for downstream expression evaluation
|
||||
// despite the error. Returning [cty.DynamicVal] is acceptable if all else
|
||||
// fails, but returning an unknown value with a more specific type
|
||||
// constraint can give more opportunities to proactively detect downstream
|
||||
// errors in a single evaluation pass.
|
||||
Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics)
|
||||
|
||||
// StaticCheckTraversal checks whether the given relative traversal would
|
||||
@@ -50,4 +57,82 @@ type Valuer interface {
|
||||
// This function should only return errors that should not be interceptable
|
||||
// by the "try" or "can" functions in the OpenTofu language.
|
||||
StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics
|
||||
|
||||
// ValueSourceRange returns an optional source range where this value (or an
|
||||
// expression that produced it) was declared in configuration.
|
||||
//
|
||||
// Returns nil for a valuer that does not come from configuration.
|
||||
ValueSourceRange() *tfdiags.SourceRange
|
||||
}
|
||||
|
||||
// ConstantValuer returns a [Valuer] that always succeeds and returns exactly
|
||||
// the value given.
|
||||
func ConstantValuer(v cty.Value) Valuer {
|
||||
return constantValuer{v, nil}
|
||||
}
|
||||
|
||||
// ConstantValuerWithSourceRange is like [ConstantValuer] except that the
|
||||
// result will also claim to have originated in the configuration at whatever
|
||||
// source range is given.
|
||||
func ConstantValuerWithSourceRange(v cty.Value, rng tfdiags.SourceRange) Valuer {
|
||||
return constantValuer{v, &rng}
|
||||
}
|
||||
|
||||
// ForcedErrorValuer returns a [Valuer] that always fails with [cty.DynamicVal]
|
||||
// as its placeholder result and with the given diagnostics, which must include
|
||||
// at least one error or this function will panic.
|
||||
//
|
||||
// This is primarily intended for unit testing purposes for creating
|
||||
// placeholders for upstream objects that have failed, but might also be useful
|
||||
// sometimes for handling unusual situations in "real" code.
|
||||
func ForcedErrorValuer(diags tfdiags.Diagnostics) Valuer {
|
||||
if !diags.HasErrors() {
|
||||
panic("ForcedErrorHandler without any error diagnostics")
|
||||
}
|
||||
return forcedErrorValuer{diags}
|
||||
}
|
||||
|
||||
type constantValuer struct {
|
||||
v cty.Value
|
||||
sourceRange *tfdiags.SourceRange
|
||||
}
|
||||
|
||||
// StaticCheckTraversal implements Valuer.
|
||||
func (c constantValuer) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
_, hclDiags := traversal.TraverseRel(c.v)
|
||||
diags = diags.Append(hclDiags)
|
||||
return diags
|
||||
}
|
||||
|
||||
// Value implements Valuer.
|
||||
func (c constantValuer) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
|
||||
return c.v, nil
|
||||
}
|
||||
|
||||
// ValueSourceRange implements Valuer.
|
||||
func (c constantValuer) ValueSourceRange() *tfdiags.SourceRange {
|
||||
return c.sourceRange
|
||||
}
|
||||
|
||||
type forcedErrorValuer struct {
|
||||
diags tfdiags.Diagnostics
|
||||
}
|
||||
|
||||
// StaticCheckTraversal implements Valuer.
|
||||
func (f forcedErrorValuer) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics {
|
||||
// This never actually produces a successful result, so there's nothing
|
||||
// to check against and we'll just wait until Value is called to return
|
||||
// our predefined diagnostics.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements Valuer.
|
||||
func (f forcedErrorValuer) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
|
||||
return cty.DynamicVal, f.diags
|
||||
}
|
||||
|
||||
// ValueSourceRange implements Valuer.
|
||||
func (f forcedErrorValuer) ValueSourceRange() *tfdiags.SourceRange {
|
||||
return nil
|
||||
}
|
||||
|
||||
72
internal/lang/grapheval/context.go
Normal file
72
internal/lang/grapheval/context.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 grapheval
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/apparentlymart/go-workgraph/workgraph"
|
||||
)
|
||||
|
||||
// ContextWithWorker returns a child of the given context that is associated
|
||||
// with the given [workgraph.Worker].
|
||||
//
|
||||
// This is a very low-level API for use by code that is interacting directly
|
||||
// with the [workgraph] package API. Callers should prefer to use the
|
||||
// higher-level wrappers in this package whenever possible, because they
|
||||
// manage context-based worker tracking automatically on the caller's behalf.
|
||||
func ContextWithWorker(parent context.Context, worker *workgraph.Worker) context.Context {
|
||||
return context.WithValue(parent, workerContextKey, worker)
|
||||
}
|
||||
|
||||
// ContextWithNewWorker is like [ContextWithWorker] except that it internally
|
||||
// creates a new worker and associates that with the returned context.
|
||||
//
|
||||
// This is a good way to create the top-level context needed for first entry
|
||||
// into a call graph that relies on the self-reference-detecting functions
|
||||
// elsewhere in this package. Those functions will then create other workers
|
||||
// as necessary.
|
||||
func ContextWithNewWorker(parent context.Context) context.Context {
|
||||
return ContextWithWorker(parent, workgraph.NewWorker())
|
||||
}
|
||||
|
||||
// WorkerFromContext returns a pointer to the [workgraph.Worker] associated
|
||||
// with the given context, or panics if the context has no worker.
|
||||
func WorkerFromContext(ctx context.Context) *workgraph.Worker {
|
||||
worker, ok := ctx.Value(workerContextKey).(*workgraph.Worker)
|
||||
if !ok {
|
||||
panic("no worker handle in this context")
|
||||
}
|
||||
return worker
|
||||
}
|
||||
|
||||
// ContextWithRequestTracker returns a child of the given context that is
|
||||
// associated with the given [RequestTracker].
|
||||
//
|
||||
// Pass promises derived from the result to other functions in this package that
|
||||
// perform self-reference and unresolved request detection to improve the
|
||||
// error messages returned when those error situations occur.
|
||||
func ContextWithRequestTracker(parent context.Context, tracker RequestTracker) context.Context {
|
||||
return context.WithValue(parent, trackerContextKey, tracker)
|
||||
}
|
||||
|
||||
// requestTrackerFromContext returns the request tracker associated with the
|
||||
// given context, or nil if there is no request tracker.
|
||||
//
|
||||
// This is unexported because request trackers are provided by external code
|
||||
// but only used by code within this package.
|
||||
func requestTrackerFromContext(ctx context.Context) RequestTracker {
|
||||
tracker, ok := ctx.Value(trackerContextKey).(RequestTracker)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return tracker
|
||||
}
|
||||
|
||||
type contextKey rune
|
||||
|
||||
const workerContextKey = contextKey('W')
|
||||
const trackerContextKey = contextKey('T')
|
||||
157
internal/lang/grapheval/diagnostics.go
Normal file
157
internal/lang/grapheval/diagnostics.go
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package grapheval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/apparentlymart/go-workgraph/workgraph"
|
||||
hcl "github.com/hashicorp/hcl/v2"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
// DiagnosticsForWorkgraphError transforms an error returned by a call to
|
||||
// [workgraph.Promise.Await], describing a problem that occurred in the
|
||||
// active request graph, into user-facing diagnostic messages describing the
|
||||
// problem.
|
||||
//
|
||||
// This function can only produce a user-friendly result when the given context
|
||||
// contains a request tracker as arranged by [ContextWithRequestTracker], and
|
||||
// that tracker is able to report all of the requests involved in the problem.
|
||||
// If that isn't true then the diagnostic messages will lack important
|
||||
// information and will report that missing information as being a bug in
|
||||
// OpenTofu, because we should always be tracking requests correctly.
|
||||
func DiagnosticsForWorkgraphError(ctx context.Context, err error) tfdiags.Diagnostics {
|
||||
tracker := requestTrackerFromContext(ctx)
|
||||
|
||||
if tracker == nil {
|
||||
// In this case we must return lower-quality error messages because
|
||||
// we don't have any way to name the affected requests. This is
|
||||
// primarily for internal callers like unit tests; we should avoid
|
||||
// getting here in any case where the result is being returned to
|
||||
// end-users.
|
||||
return diagnosticsForWorkgraphErrorUntracked(err)
|
||||
}
|
||||
|
||||
// In the most happy case we have an active request tracker and so we
|
||||
// should be able to describe the individial requests that were impacted
|
||||
// by this problem.
|
||||
return diagnosticsForWorkgraphErrorTracked(err, tracker)
|
||||
}
|
||||
|
||||
func diagnosticsForWorkgraphErrorTracked(err error, tracker RequestTracker) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
switch err := err.(type) {
|
||||
case workgraph.ErrSelfDependency:
|
||||
// This is the only case that (probably) doesn't represent a bug in
|
||||
// OpenTofu: we will get in here if OpenTofu is tracking everything
|
||||
// correctly but the configuration contains an expression that depends
|
||||
// on its own result, directly or indirectly.
|
||||
reqInfos := collectRequestsInfo(slices.Values(err.RequestIDs), tracker)
|
||||
var detailBuf strings.Builder
|
||||
detailBuf.WriteString("The following objects in the configuration form a dependency cycle, so there is no valid order to evaluate them in:\n")
|
||||
for _, reqID := range err.RequestIDs {
|
||||
desc := "<unknown object> (failing to report this is a bug in OpenTofu)"
|
||||
if info := reqInfos[reqID]; info != nil {
|
||||
if info.SourceRange != nil {
|
||||
desc = fmt.Sprintf("%s (%s)", info.Name, info.SourceRange.StartString())
|
||||
} else {
|
||||
desc = info.Name
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&detailBuf, " - %s\n", desc)
|
||||
}
|
||||
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Self-referential expressions",
|
||||
"The configuration contains expressions that form a dependency cycle.",
|
||||
))
|
||||
case workgraph.ErrUnresolved:
|
||||
reqName := "<unknown request>"
|
||||
var sourceRange *hcl.Range
|
||||
|
||||
reqInfos := collectRequestsInfo(oneSeq(err.RequestID), tracker)
|
||||
if reqInfo := reqInfos[err.RequestID]; reqInfo != nil {
|
||||
reqName = reqInfo.Name
|
||||
if reqInfo.SourceRange != nil {
|
||||
sourceRange = reqInfo.SourceRange.ToHCL().Ptr()
|
||||
}
|
||||
}
|
||||
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Configuration evaluation failed",
|
||||
Detail: fmt.Sprintf("During configuration evaluation, %q was left unresolved. This is a bug in OpenTofu.", reqName),
|
||||
Subject: sourceRange,
|
||||
})
|
||||
default:
|
||||
// We should not get here because the two cases above cover everything
|
||||
// that package workgraph should return.
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Evaluation failed",
|
||||
"Configuration evaluation failed for an unknown reason. This is a bug in OpenTofu.",
|
||||
))
|
||||
}
|
||||
return diags
|
||||
}
|
||||
|
||||
func diagnosticsForWorkgraphErrorUntracked(err error) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
switch err.(type) {
|
||||
case workgraph.ErrUnresolved:
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Evaluation failed",
|
||||
"An unexpected problem prevented complete evaluation of the configuration. This is a bug in OpenTofu.",
|
||||
))
|
||||
case workgraph.ErrSelfDependency:
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Self-referential expressions",
|
||||
"The configuration contains expressions that form a dependency cycle. Unfortunately, a bug in OpenTofu prevents reporting the affected expressions.",
|
||||
))
|
||||
default:
|
||||
// We should not get here because the two cases above cover everything
|
||||
// that package workgraph should return.
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Evaluation failed",
|
||||
"Configuration evaluation failed for an unknown reason. This is a bug in OpenTofu.",
|
||||
))
|
||||
}
|
||||
return diags
|
||||
}
|
||||
|
||||
// collectRequestsInfo collects a [RequestInfo] value for each of the given
|
||||
// request IDs that is known to the given tracker, or reports nil for any
|
||||
// request ID that is not known to the tracker.
|
||||
func collectRequestsInfo(requestIDs iter.Seq[workgraph.RequestID], tracker RequestTracker) map[workgraph.RequestID]*RequestInfo {
|
||||
ret := make(map[workgraph.RequestID]*RequestInfo)
|
||||
for requestID := range requestIDs {
|
||||
ret[requestID] = nil
|
||||
}
|
||||
for requestID, info := range tracker.ActiveRequests() {
|
||||
if _, ok := ret[requestID]; ok {
|
||||
ret[requestID] = &info
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// FIXME: This is a placeholder for what's proposed here but not yet accepted
|
||||
// at the time of writing: https://github.com/golang/go/issues/68947
|
||||
func oneSeq[T any](value T) iter.Seq[T] {
|
||||
return func(yield func(T) bool) {
|
||||
yield(value)
|
||||
}
|
||||
}
|
||||
14
internal/lang/grapheval/doc.go
Normal file
14
internal/lang/grapheval/doc.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package grapheval contains some low-level helpers for coordinating
|
||||
// interdependent work happening across different parts of the system, including
|
||||
// detection and reporting of self-dependency problems that would otherwise
|
||||
// cause a deadlock.
|
||||
//
|
||||
// The symbols in this package are intended to be implementation details of
|
||||
// functionality in other packages, and not exposed as part of the public API
|
||||
// of those packages.
|
||||
package grapheval
|
||||
89
internal/lang/grapheval/once.go
Normal file
89
internal/lang/grapheval/once.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 grapheval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/apparentlymart/go-workgraph/workgraph"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
// Once is a similar principle to the Go standard library's [sync.Once], but
|
||||
// with some additions tailored for use in OpenTofu:
|
||||
//
|
||||
// - The "Do" method returns a result of type T and diagnostics.
|
||||
// - If two Once instances attempt to depend on each other for resolution then
|
||||
// both will immediately fail with error diagnostics, rather than deadlocking.
|
||||
//
|
||||
// The typical way to use this type is to use is as a field of a type
|
||||
// representing whatever object the operation conceptually belongs to, and
|
||||
// then offer a method of that type which wraps the call to [Once.Do], thus
|
||||
// ensuring that the first call to that method will cause the one-time operation
|
||||
// to start and then subsequent calls will return the same result as that
|
||||
// first call, without directly exposing this type in the public-facing
|
||||
// signature.
|
||||
type Once[T any] struct {
|
||||
mu sync.Mutex
|
||||
promise *workgraph.Promise[withDiagnostics[T]]
|
||||
requestID workgraph.RequestID
|
||||
}
|
||||
|
||||
// The first time Do is called it runs the given function and returns its result
|
||||
// once complete. Subsequent calls then just wait for the result of the function
|
||||
// passed in the first call and return its result.
|
||||
//
|
||||
// The given context MUST have an associated [workgraph.Worker]. Typically
|
||||
// the first worker should be established on entry to an internal callgraph
|
||||
// that relies on this package, by calling [ContextWithNewWorker].
|
||||
//
|
||||
// The automatic deadlock detection relies on consistent use of
|
||||
// [context.Context] values: any other calls to [once.Do]
|
||||
// made directly or indirectly from the given callback function on any
|
||||
// Once object in the program MUST pass a context derived from the one passed
|
||||
// into the callback function, because it includes internal tracking
|
||||
// information.
|
||||
func (o *Once[T]) Do(ctx context.Context, f func(ctx context.Context) (T, tfdiags.Diagnostics)) (T, tfdiags.Diagnostics) {
|
||||
worker := WorkerFromContext(ctx)
|
||||
o.mu.Lock()
|
||||
if o.promise == nil {
|
||||
// This is the first call, so we'll establish the inner request and
|
||||
// start executing the function in a separate goroutine.
|
||||
resolver, promise := workgraph.NewRequest[withDiagnostics[T]](worker)
|
||||
o.promise = &promise
|
||||
o.requestID = resolver.RequestID()
|
||||
workgraph.WithNewAsyncWorker(func(w *workgraph.Worker) {
|
||||
ctx := ContextWithWorker(ctx, w)
|
||||
ret, diags := f(ctx)
|
||||
resolver.Report(w, withDiagnostics[T]{ret, diags}, nil)
|
||||
}, resolver)
|
||||
}
|
||||
o.mu.Unlock()
|
||||
|
||||
withDiags, err := o.promise.Await(worker)
|
||||
if err != nil {
|
||||
// We return our own errors only as diagnostics, so any error here
|
||||
// must be one generated by the workgraph package itself in response
|
||||
// to a problem such as self-reference or a failure to resolve some
|
||||
// other request.
|
||||
var zero T
|
||||
return zero, DiagnosticsForWorkgraphError(ctx, err)
|
||||
}
|
||||
return withDiags.value, withDiags.diags
|
||||
}
|
||||
|
||||
// RequestID returns the [workgraph.RequestID] associated with the inner
|
||||
// request, or [workgraph.NoRequest] if Do has not been called yet.
|
||||
func (o *Once[T]) RequestID() workgraph.RequestID {
|
||||
return o.requestID
|
||||
}
|
||||
|
||||
type withDiagnostics[T any] struct {
|
||||
value T
|
||||
diags tfdiags.Diagnostics
|
||||
}
|
||||
45
internal/lang/grapheval/request_tracker.go
Normal file
45
internal/lang/grapheval/request_tracker.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package grapheval
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/apparentlymart/go-workgraph/workgraph"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
// RequestTracker is implemented by types that know how to provide user-friendly
|
||||
// descriptions for all active requests in a particular context for error
|
||||
// reporting purposes.
|
||||
//
|
||||
// This is designed to allow delaying at least some of the work required to
|
||||
// build user-friendly error messages about eval-request-related problems until
|
||||
// an error actually occurs, because we don't need this information at all in
|
||||
// the happy path.
|
||||
//
|
||||
// Use [ContextWithRequestTracker] to associate a request tracker with a
|
||||
// [context.Context], and then pass contexts derived from that one to the
|
||||
// other functions in this package that perform self-dependency and unresolved
|
||||
// request detection to allow those operations to return better diagnostic
|
||||
// messages when those situations occur.
|
||||
type RequestTracker interface {
|
||||
// ActiveRequests returns an iterable sequence of all active requests
|
||||
// known to the tracker, along with the [RequestInfo] for each one.
|
||||
ActiveRequests() iter.Seq2[workgraph.RequestID, RequestInfo]
|
||||
}
|
||||
|
||||
type RequestInfo struct {
|
||||
// Name is a short, user-friendly name for whatever this request was trying
|
||||
// to calculate.
|
||||
Name string
|
||||
|
||||
// SourceRange is an optional source range for something in the
|
||||
// configuration that caused this request to be made. Leave this nil
|
||||
// for requests that aren't clearly related to a specific element in
|
||||
// the given configuration.
|
||||
SourceRange *tfdiags.SourceRange
|
||||
}
|
||||
Reference in New Issue
Block a user