Static Evaluation Base, Module Sources, Backend Config (#1718)

Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
Signed-off-by: Christian Mesh <cristianmesh1@gmail.com>
Co-authored-by: James Humphries <James@james-humphries.co.uk>
Co-authored-by: Oleksandr Levchenkov <ollevche@gmail.com>
This commit is contained in:
Christian Mesh 2024-06-24 09:13:07 -04:00 committed by GitHub
parent ab289fc07c
commit 8f8e0aa4aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
88 changed files with 1556 additions and 404 deletions

View File

@ -5,6 +5,7 @@ BREAKING CHANGE - `use_legacy_workflow` field has been removing from the S3 back
NEW FEATURES:
* Added support for `override_resource`, `override_data` and `override_module` blocks in testing framework. ([1499](https://github.com/opentofu/opentofu/pull/1499))
* Variables and Locals allowed in module sources and backend configurations (with limitations) ([1718](https://github.com/opentofu/opentofu/pull/1718))
ENHANCEMENTS:
* Added `tofu test -json` types to website Machine-Readable UI documentation. ([1408](https://github.com/opentofu/opentofu/issues/1408))

View File

@ -283,7 +283,9 @@ type Operation struct {
AutoApprove bool
Targets []addrs.Targetable
ForceReplace []addrs.AbsResourceInstance
Variables map[string]UnparsedVariableValue
// Injected by the command creating the operation (plan/apply/refresh/etc...)
Variables map[string]UnparsedVariableValue
RootCall configs.StaticModuleCall
// Some operations use root module variables only opportunistically or
// don't need them at all. If this flag is set, the backend must treat
@ -326,15 +328,6 @@ func (o *Operation) HasConfig() bool {
return o.ConfigLoader.IsConfigDir(o.ConfigDir)
}
// Config loads the configuration that the operation applies to, using the
// ConfigDir and ConfigLoader fields within the receiving operation.
func (o *Operation) Config() (*configs.Config, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
config, hclDiags := o.ConfigLoader.LoadConfig(o.ConfigDir)
diags = diags.Append(hclDiags)
return config, diags
}
// ReportResult is a helper for the common chore of setting the status of
// a running operation and showing any diagnostics produced during that
// operation.

View File

@ -145,7 +145,7 @@ func (b *Local) localRunDirect(op *backend.Operation, run *backend.LocalRun, cor
var diags tfdiags.Diagnostics
// Load the configuration using the caller-provided configuration loader.
config, configSnap, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir)
config, configSnap, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir, op.RootCall)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
return nil, nil, diags
@ -249,7 +249,7 @@ func (b *Local) localRunForPlanFile(op *backend.Operation, pf *planfile.Reader,
return nil, snap, diags
}
loader := configload.NewLoaderFromSnapshot(snap)
config, configDiags := loader.LoadConfig(snap.Modules[""].Dir)
config, configDiags := loader.LoadConfig(snap.Modules[""].Dir, op.RootCall)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
return nil, snap, diags

View File

@ -223,7 +223,7 @@ func (b *Remote) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Oper
// remote system's responsibility to do final validation of the input.
func (b *Remote) hasExplicitVariableValues(op *backend.Operation) bool {
// Load the configuration using the caller-provided configuration loader.
config, _, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir)
config, _, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir, op.RootCall)
if configDiags.HasErrors() {
// If we can't load the configuration then we'll assume no explicit
// variable values just to let the remote operation start and let

View File

@ -81,7 +81,7 @@ func (b *Remote) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Fu
ret.InputState = stateMgr.State()
log.Printf("[TRACE] backend/remote: loading configuration for the current working directory")
config, configDiags := op.ConfigLoader.LoadConfig(op.ConfigDir)
config, configDiags := op.ConfigLoader.LoadConfig(op.ConfigDir, op.RootCall)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
return nil, nil, diags

View File

@ -12,6 +12,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configload"
"github.com/opentofu/opentofu/internal/initwd"
)
@ -21,7 +22,7 @@ func TestChecksHappyPath(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, nil)
_, instDiags := inst.InstallModules(context.Background(), fixtureDir, "tests", true, false, initwd.ModuleInstallHooksImpl{})
_, instDiags := inst.InstallModules(context.Background(), fixtureDir, "tests", true, false, initwd.ModuleInstallHooksImpl{}, configs.RootModuleCallForTesting())
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())
}
@ -31,7 +32,7 @@ func TestChecksHappyPath(t *testing.T) {
/////////////////////////////////////////////////////////////////////////
cfg, hclDiags := loader.LoadConfig(fixtureDir)
cfg, hclDiags := loader.LoadConfig(fixtureDir, configs.RootModuleCallForTesting())
if hclDiags.HasErrors() {
t.Fatalf("invalid configuration: %s", hclDiags.Error())
}

View File

@ -81,7 +81,7 @@ func (b *Cloud) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Ful
ret.InputState = stateMgr.State()
log.Printf("[TRACE] cloud: loading configuration for the current working directory")
config, configDiags := op.ConfigLoader.LoadConfig(op.ConfigDir)
config, configDiags := op.ConfigLoader.LoadConfig(op.ConfigDir, op.RootCall)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
return nil, nil, diags

View File

@ -256,7 +256,7 @@ in order to capture the filesystem context the remote workspace expects:
}
}
config, _, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir)
config, _, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir, op.RootCall)
if configDiags.HasErrors() {
return nil, fmt.Errorf("error loading config with snapshot: %w", configDiags.Errs()[0])
}

View File

@ -67,6 +67,9 @@ func (c *ApplyCommand) Run(rawArgs []string) int {
return 1
}
// Inject variables from args into meta for static evaluation
c.GatherVariables(args.Vars)
// Load the encryption configuration
enc, encDiags := c.Encryption()
diags = diags.Append(encDiags)
@ -119,9 +122,6 @@ func (c *ApplyCommand) Run(rawArgs []string) int {
opReq, opDiags := c.OperationRequest(be, view, args.ViewType, planFile, args.Operation, args.AutoApprove, enc)
diags = diags.Append(opDiags)
// Collect variable value and add them to the operation request
diags = diags.Append(c.GatherVariables(opReq, args.Vars))
// Before we delegate to the backend, we'll print any warning diagnostics
// we've accumulated here, since the backend will start fresh with its own
// diagnostics.
@ -132,10 +132,9 @@ func (c *ApplyCommand) Run(rawArgs []string) int {
diags = nil
// Run the operation
op, err := c.RunOperation(be, opReq)
if err != nil {
diags = diags.Append(err)
view.Diagnostics(diags)
op, diags := c.RunOperation(be, opReq)
view.Diagnostics(diags)
if diags.HasErrors() {
return 1
}
@ -297,9 +296,7 @@ func (c *ApplyCommand) OperationRequest(
return opReq, diags
}
func (c *ApplyCommand) GatherVariables(opReq *backend.Operation, args *arguments.Vars) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
func (c *ApplyCommand) GatherVariables(args *arguments.Vars) {
// FIXME the arguments package currently trivially gathers variable related
// arguments in a heterogenous slice, in order to minimize the number of
// code paths gathering variables during the transition to this structure.
@ -314,9 +311,6 @@ func (c *ApplyCommand) GatherVariables(opReq *backend.Operation, args *arguments
items[i].Value = varArgs[i].Value
}
c.Meta.variableArgs = rawFlags{items: &items}
opReq.Variables, diags = c.collectVariableValues()
return diags
}
func (c *ApplyCommand) Help() string {

View File

@ -21,6 +21,8 @@ type Output struct {
// ViewType specifies which output format to use: human, JSON, or "raw".
ViewType ViewType
Vars *Vars
}
// ParseOutput processes CLI arguments, returning an Output value and errors.
@ -28,11 +30,13 @@ type Output struct {
// the best effort interpretation of the arguments.
func ParseOutput(args []string) (*Output, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
output := &Output{}
output := &Output{
Vars: &Vars{},
}
var jsonOutput, rawOutput bool
var statePath string
cmdFlags := defaultFlagSet("output")
cmdFlags := extendedFlagSet("output", nil, nil, output.Vars)
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
cmdFlags.BoolVar(&rawOutput, "raw", false, "raw")
cmdFlags.StringVar(&statePath, "state", "", "path")

View File

@ -58,6 +58,7 @@ func TestParseOutput_valid(t *testing.T) {
if len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
}
got.Vars = nil
if *got != *tc.want {
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
}
@ -136,6 +137,7 @@ func TestParseOutput_invalid(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, gotDiags := ParseOutput(tc.args)
got.Vars = nil
if *got != *tc.want {
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
}

View File

@ -17,6 +17,8 @@ type Show struct {
// ViewType specifies which output format to use: human, JSON, or "raw".
ViewType ViewType
Vars *Vars
}
// ParseShow processes CLI arguments, returning a Show value and errors.
@ -26,10 +28,11 @@ func ParseShow(args []string) (*Show, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
show := &Show{
Path: "",
Vars: &Vars{},
}
var jsonOutput bool
cmdFlags := defaultFlagSet("show")
cmdFlags := extendedFlagSet("show", nil, nil, show.Vars)
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
if err := cmdFlags.Parse(args); err != nil {

View File

@ -44,6 +44,7 @@ func TestParseShow_valid(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, diags := ParseShow(tc.args)
got.Vars = nil
if len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
}
@ -93,6 +94,7 @@ func TestParseShow_invalid(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, gotDiags := ParseShow(tc.args)
got.Vars = nil
if *got != *tc.want {
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
}

View File

@ -162,12 +162,12 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config
// sources only this ultimately just records all of the module paths
// in a JSON file so that we can load them below.
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{})
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{}, configs.RootModuleCallForTesting())
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())
}
config, snap, diags := loader.LoadConfigWithSnapshot(dir)
config, snap, diags := loader.LoadConfigWithSnapshot(dir, configs.RootModuleCallForTesting())
if diags.HasErrors() {
t.Fatal(diags.Error())
}
@ -773,10 +773,11 @@ func testBackendState(t *testing.T, s *states.State, c int) (*legacy.State, *htt
backendConfig := &configs.Backend{
Type: "http",
Config: configs.SynthBody("<testBackendState>", map[string]cty.Value{}),
Eval: configs.NewStaticEvaluator(nil, configs.RootModuleCallForTesting()),
}
b := backendInit.Backend("http")(encryption.StateEncryptionDisabled())
configSchema := b.ConfigSchema()
hash := backendConfig.Hash(configSchema)
hash, _ := backendConfig.Hash(configSchema)
state := legacy.NewState()
state.Backend = &legacy.BackendState{

View File

@ -100,9 +100,11 @@ func (c *ConsoleCommand) Run(args []string) int {
}
{
var moreDiags tfdiags.Diagnostics
// Setup required variables/call for operation (usually done in Meta.RunOperation)
var moreDiags, callDiags tfdiags.Diagnostics
opReq.Variables, moreDiags = c.collectVariableValues()
diags = diags.Append(moreDiags)
opReq.RootCall, callDiags = c.rootModuleCall(opReq.ConfigDir)
diags = diags.Append(moreDiags).Append(callDiags)
if moreDiags.HasErrors() {
c.showDiagnostics(diags)
return 1

View File

@ -35,6 +35,7 @@ func (c *GraphCommand) Run(args []string) int {
args = c.Meta.process(args)
cmdFlags := c.Meta.defaultFlagSet("graph")
c.Meta.varFlagSet(cmdFlags)
cmdFlags.BoolVar(&drawCycles, "draw-cycles", false, "draw-cycles")
cmdFlags.StringVar(&graphTypeStr, "type", "", "type")
cmdFlags.IntVar(&moduleDepth, "module-depth", -1, "module-depth")

View File

@ -204,9 +204,11 @@ func (c *ImportCommand) Run(args []string) int {
}
opReq.Hooks = []tofu.Hook{c.uiHook()}
{
var moreDiags tfdiags.Diagnostics
// Setup required variables/call for operation (usually done in Meta.RunOperation)
var moreDiags, callDiags tfdiags.Diagnostics
opReq.Variables, moreDiags = c.collectVariableValues()
diags = diags.Append(moreDiags)
opReq.RootCall, callDiags = c.rootModuleCall(opReq.ConfigDir)
diags = diags.Append(moreDiags).Append(callDiags)
if moreDiags.HasErrors() {
c.showDiagnostics(diags)
return 1

View File

@ -2921,6 +2921,80 @@ func TestInit_testsWithModule(t *testing.T) {
}
}
// Test variables are handled correctly when interacting with module sources
func TestInit_moduleSource(t *testing.T) {
t.Run("missing", func(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("init-module-variable-source"), td)
defer testChdir(t, td)()
ui := cli.NewMockUi()
view, _ := testView(t)
c := &InitCommand{
Meta: Meta{
Ui: ui,
View: view,
},
}
if code := c.Run(nil); code != 1 {
t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String())
}
errStr := ui.ErrorWriter.String()
if !strings.Contains(errStr, `Variable not provided`) {
t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr)
}
})
t.Run("provided", func(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("init-module-variable-source"), td)
defer testChdir(t, td)()
ui := cli.NewMockUi()
view, _ := testView(t)
c := &InitCommand{
Meta: Meta{
Ui: ui,
View: view,
},
}
args := []string{"-var", "src=./mod"}
if code := c.Run(args); code != 0 {
t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String())
}
})
}
// Test variables are handled correctly when interacting with module versions
func TestInit_moduleVersion(t *testing.T) {
if os.Getenv("TF_ACC") == "" {
t.Skip("network access not allowed; use TF_ACC=1 to enable")
}
t.Run("provided", func(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("init-module-variable-version"), td)
defer testChdir(t, td)()
ui := cli.NewMockUi()
view, _ := testView(t)
c := &InitCommand{
Meta: Meta{
Ui: ui,
View: view,
},
}
args := []string{"-var", "modver=0.0.1"}
if code := c.Run(args); code != 0 {
t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String())
}
})
}
// newMockProviderSource is a helper to succinctly construct a mock provider
// source that contains a set of packages matching the given provider versions
// that are available for installation (from temporary local files).

View File

@ -268,6 +268,11 @@ type Meta struct {
ignoreRemoteVersion bool
outputInJSON bool
// Used to cache the root module rootModuleCallCache and known variables.
// This helps prevent duplicate errors/warnings.
rootModuleCallCache *configs.StaticModuleCall
inputVariableCache map[string]backend.UnparsedVariableValue
}
type testingOverrides struct {
@ -471,7 +476,7 @@ func (m *Meta) CommandContext() context.Context {
// If the operation runs to completion then no error is returned even if the
// operation itself is unsuccessful. Use the "Result" field of the
// returned operation object to recognize operation-level failure.
func (m *Meta) RunOperation(b backend.Enhanced, opReq *backend.Operation) (*backend.RunningOperation, error) {
func (m *Meta) RunOperation(b backend.Enhanced, opReq *backend.Operation) (*backend.RunningOperation, tfdiags.Diagnostics) {
if opReq.View == nil {
panic("RunOperation called with nil View")
}
@ -479,9 +484,18 @@ func (m *Meta) RunOperation(b backend.Enhanced, opReq *backend.Operation) (*back
opReq.ConfigDir = m.normalizePath(opReq.ConfigDir)
}
// Inject variables and root module call
var diags, callDiags tfdiags.Diagnostics
opReq.Variables, diags = m.collectVariableValues()
opReq.RootCall, callDiags = m.rootModuleCall(opReq.ConfigDir)
diags = diags.Append(callDiags)
if diags.HasErrors() {
return nil, diags
}
op, err := b.Operation(context.Background(), opReq)
if err != nil {
return nil, fmt.Errorf("error starting operation: %w", err)
return nil, diags.Append(fmt.Errorf("error starting operation: %w", err))
}
// Wait for the operation to complete or an interrupt to occur
@ -508,7 +522,7 @@ func (m *Meta) RunOperation(b backend.Enhanced, opReq *backend.Operation) (*back
case <-time.After(5 * time.Second):
}
return nil, errors.New("operation canceled")
return nil, diags.Append(errors.New("operation canceled"))
case <-op.Done():
// operation completed after Stop
@ -517,7 +531,7 @@ func (m *Meta) RunOperation(b backend.Enhanced, opReq *backend.Operation) (*back
// operation completed normally
}
return op, nil
return op, diags
}
// contextOpts returns the options to use to initialize a OpenTofu
@ -572,11 +586,23 @@ func (m *Meta) defaultFlagSet(n string) *flag.FlagSet {
func (m *Meta) ignoreRemoteVersionFlagSet(n string) *flag.FlagSet {
f := m.defaultFlagSet(n)
m.varFlagSet(f)
f.BoolVar(&m.ignoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local OpenTofu versions are incompatible")
return f
}
func (m *Meta) varFlagSet(f *flag.FlagSet) {
if m.variableArgs.items == nil {
m.variableArgs = newRawFlags("-var")
}
varValues := m.variableArgs.Alias("-var")
varFiles := m.variableArgs.Alias("-var-file")
f.Var(varValues, "var", "variables")
f.Var(varFiles, "var-file", "variable file")
}
// extendedFlagSet adds custom flags that are mostly used by commands
// that are used to run an operation like plan or apply.
func (m *Meta) extendedFlagSet(n string) *flag.FlagSet {
@ -586,13 +612,7 @@ func (m *Meta) extendedFlagSet(n string) *flag.FlagSet {
f.Var((*FlagStringSlice)(&m.targetFlags), "target", "resource to target")
f.BoolVar(&m.compactWarnings, "compact-warnings", false, "use compact warnings")
if m.variableArgs.items == nil {
m.variableArgs = newRawFlags("-var")
}
varValues := m.variableArgs.Alias("-var")
varFiles := m.variableArgs.Alias("-var-file")
f.Var(varValues, "var", "variables")
f.Var(varFiles, "var-file", "variable file")
m.varFlagSet(f)
// commands that bypass locking will supply their own flag on this var,
// but set the initial meta value to true as a failsafe.
@ -837,7 +857,13 @@ func (m *Meta) checkRequiredVersion() tfdiags.Diagnostics {
return diags
}
config, configDiags := loader.LoadConfig(pwd)
call, callDiags := m.rootModuleCall(pwd)
if callDiags.HasErrors() {
diags = diags.Append(callDiags)
return diags
}
config, configDiags := loader.LoadConfig(pwd, call)
if configDiags.HasErrors() {
diags = diags.Append(configDiags)
return diags

View File

@ -499,7 +499,11 @@ func (m *Meta) backendConfig(opts *BackendOpts) (*configs.Backend, int, tfdiags.
configSchema := b.ConfigSchema()
configBody := c.Config
configHash := c.Hash(configSchema)
configHash, cfgDiags := c.Hash(configSchema)
diags = diags.Append(cfgDiags)
if diags.HasErrors() {
return nil, 0, diags
}
// If we have an override configuration body then we must apply it now.
if opts.ConfigOverride != nil {
@ -1396,8 +1400,7 @@ func (m *Meta) backendInitFromConfig(c *configs.Backend, enc encryption.StateEnc
b := f(enc)
schema := b.ConfigSchema()
decSpec := schema.NoneRequired().DecoderSpec()
configVal, hclDiags := hcldec.Decode(c.Config, decSpec, nil)
configVal, hclDiags := c.Decode(schema.NoneRequired())
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
return nil, cty.NilVal, diags

View File

@ -222,7 +222,7 @@ func TestMetaBackend_emptyWithExplicitState(t *testing.T) {
}
}
// Verify that interpolations result in an error
// Verify that interpolations are allowed
func TestMetaBackend_configureInterpolation(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
@ -234,8 +234,8 @@ func TestMetaBackend_configureInterpolation(t *testing.T) {
// Get the backend
_, err := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled())
if err == nil {
t.Fatal("should error")
if err != nil {
t.Fatal("should not error")
}
}

View File

@ -19,6 +19,7 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configload"
"github.com/opentofu/opentofu/internal/configs/configschema"
@ -50,7 +51,13 @@ func (m *Meta) loadConfig(rootDir string) (*configs.Config, tfdiags.Diagnostics)
return nil, diags
}
config, hclDiags := loader.LoadConfig(rootDir)
call, callDiags := m.rootModuleCall(rootDir)
diags = diags.Append(callDiags)
if callDiags.HasErrors() {
return nil, diags
}
config, hclDiags := loader.LoadConfig(rootDir, call)
diags = diags.Append(hclDiags)
return config, diags
}
@ -67,7 +74,13 @@ func (m *Meta) loadConfigWithTests(rootDir, testDir string) (*configs.Config, tf
return nil, diags
}
config, hclDiags := loader.LoadConfigWithTests(rootDir, testDir)
call, vDiags := m.rootModuleCall(rootDir)
diags = diags.Append(vDiags)
if diags.HasErrors() {
return nil, diags
}
config, hclDiags := loader.LoadConfigWithTests(rootDir, testDir, call)
diags = diags.Append(hclDiags)
return config, diags
}
@ -90,11 +103,53 @@ func (m *Meta) loadSingleModule(dir string) (*configs.Module, tfdiags.Diagnostic
return nil, diags
}
module, hclDiags := loader.Parser().LoadConfigDir(dir)
call, vDiags := m.rootModuleCall(dir)
diags = diags.Append(vDiags)
if diags.HasErrors() {
return nil, diags
}
module, hclDiags := loader.Parser().LoadConfigDir(dir, call)
diags = diags.Append(hclDiags)
return module, diags
}
func (m *Meta) rootModuleCall(rootDir string) (configs.StaticModuleCall, tfdiags.Diagnostics) {
if m.rootModuleCallCache != nil {
return *m.rootModuleCallCache, nil
}
variables, diags := m.collectVariableValues()
workspace, err := m.Workspace()
if err != nil {
diags = diags.Append(err)
}
call := configs.NewStaticModuleCall(addrs.RootModule, func(variable *configs.Variable) (cty.Value, hcl.Diagnostics) {
name := variable.Name
v, ok := variables[name]
if !ok {
// For now, we are simply failing when the user omits a required variable. This is due to complex interactions between variables in different code paths (apply existing plan for example)
// Ideally, we should be able to use something like backend_local.go:interactiveCollectVariables() in the future to allow users to provide values if input is enabled
// This is probably blocked by command package refactoring
if variable.Required() {
// Not specified on CLI or in var files, without a valid default.
return cty.NilVal, hcl.Diagnostics{&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Variable not provided via -var, tfvars files, or env",
Subject: variable.DeclRange.Ptr(),
}}
}
return variable.Default, nil
}
parsed, parsedDiags := v.ParseVariableValue(variable.ParsingMode)
return parsed.Value, parsedDiags.ToHCL()
}, rootDir, workspace)
m.rootModuleCallCache = &call
return call, diags
}
// loadSingleModuleWithTests matches loadSingleModule except it also loads any
// tests for the target module.
func (m *Meta) loadSingleModuleWithTests(dir string, testDir string) (*configs.Module, tfdiags.Diagnostics) {
@ -107,7 +162,13 @@ func (m *Meta) loadSingleModuleWithTests(dir string, testDir string) (*configs.M
return nil, diags
}
module, hclDiags := loader.Parser().LoadConfigDirWithTests(dir, testDir)
call, vDiags := m.rootModuleCall(dir)
diags = diags.Append(vDiags)
if diags.HasErrors() {
return nil, diags
}
module, hclDiags := loader.Parser().LoadConfigDirWithTests(dir, testDir, call)
diags = diags.Append(hclDiags)
return module, diags
}
@ -205,7 +266,13 @@ func (m *Meta) installModules(ctx context.Context, rootDir, testsDir string, upg
inst := initwd.NewModuleInstaller(m.modulesDir(), loader, m.registryClient())
_, moreDiags := inst.InstallModules(ctx, rootDir, testsDir, upgrade, installErrsOnly, hooks)
call, vDiags := m.rootModuleCall(rootDir)
diags = diags.Append(vDiags)
if diags.HasErrors() {
return true, diags
}
_, moreDiags := inst.InstallModules(ctx, rootDir, testsDir, upgrade, installErrsOnly, hooks, call)
diags = diags.Append(moreDiags)
if ctx.Err() == context.Canceled {

View File

@ -34,6 +34,11 @@ const VarEnvPrefix = "TF_VAR_"
// parsed.
func (m *Meta) collectVariableValues() (map[string]backend.UnparsedVariableValue, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
if m.inputVariableCache != nil {
return m.inputVariableCache, nil
}
ret := map[string]backend.UnparsedVariableValue{}
// First we'll deal with environment variables, since they have the lowest
@ -114,6 +119,7 @@ func (m *Meta) collectVariableValues() (map[string]backend.UnparsedVariableValue
diags = diags.Append(fmt.Errorf("unsupported variable option name %q (this is a bug in OpenTofu)", rawFlag.Name))
}
}
m.inputVariableCache = ret
return ret, diags
}

View File

@ -37,6 +37,9 @@ func (c *OutputCommand) Run(rawArgs []string) int {
view := views.NewOutput(args.ViewType, c.View)
// Inject variables from args into meta for static evaluation
c.GatherVariables(args.Vars)
// Load the encryption configuration
enc, encDiags := c.Encryption()
diags = diags.Append(encDiags)
@ -104,6 +107,23 @@ func (c *OutputCommand) Outputs(statePath string, enc encryption.Encryption) (ma
return output, diags
}
func (c *OutputCommand) GatherVariables(args *arguments.Vars) {
// FIXME the arguments package currently trivially gathers variable related
// arguments in a heterogenous slice, in order to minimize the number of
// code paths gathering variables during the transition to this structure.
// Once all commands that gather variables have been converted to this
// structure, we could move the variable gathering code to the arguments
// package directly, removing this shim layer.
varArgs := args.All()
items := make([]rawFlag, len(varArgs))
for i := range varArgs {
items[i].Name = varArgs[i].Name
items[i].Value = varArgs[i].Value
}
c.Meta.variableArgs = rawFlags{items: &items}
}
func (c *OutputCommand) Help() string {
helpText := `
Usage: tofu [global options] output [options] [NAME]

View File

@ -69,6 +69,9 @@ func (c *PlanCommand) Run(rawArgs []string) int {
diags = diags.Append(c.providerDevOverrideRuntimeWarnings())
// Inject variables from args into meta for static evaluation
c.GatherVariables(args.Vars)
// Load the encryption configuration
enc, encDiags := c.Encryption()
diags = diags.Append(encDiags)
@ -93,13 +96,6 @@ func (c *PlanCommand) Run(rawArgs []string) int {
return 1
}
// Collect variable value and add them to the operation request
diags = diags.Append(c.GatherVariables(opReq, args.Vars))
if diags.HasErrors() {
view.Diagnostics(diags)
return 1
}
// Before we delegate to the backend, we'll print any warning diagnostics
// we've accumulated here, since the backend will start fresh with its own
// diagnostics.
@ -107,10 +103,9 @@ func (c *PlanCommand) Run(rawArgs []string) int {
diags = nil
// Perform the operation
op, err := c.RunOperation(be, opReq)
if err != nil {
diags = diags.Append(err)
view.Diagnostics(diags)
op, diags := c.RunOperation(be, opReq)
view.Diagnostics(diags)
if diags.HasErrors() {
return 1
}
@ -183,9 +178,7 @@ func (c *PlanCommand) OperationRequest(
return opReq, diags
}
func (c *PlanCommand) GatherVariables(opReq *backend.Operation, args *arguments.Vars) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
func (c *PlanCommand) GatherVariables(args *arguments.Vars) {
// FIXME the arguments package currently trivially gathers variable related
// arguments in a heterogenous slice, in order to minimize the number of
// code paths gathering variables during the transition to this structure.
@ -200,9 +193,6 @@ func (c *PlanCommand) GatherVariables(opReq *backend.Operation, args *arguments.
items[i].Value = varArgs[i].Value
}
c.Meta.variableArgs = rawFlags{items: &items}
opReq.Variables, diags = c.collectVariableValues()
return diags
}
func (c *PlanCommand) Help() string {

View File

@ -36,6 +36,7 @@ func (c *ProvidersCommand) Run(args []string) int {
args = c.Meta.process(args)
cmdFlags := c.Meta.defaultFlagSet("providers")
c.Meta.varFlagSet(cmdFlags)
cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {

View File

@ -41,6 +41,7 @@ func (c *ProvidersLockCommand) Synopsis() string {
func (c *ProvidersLockCommand) Run(args []string) int {
args = c.Meta.process(args)
cmdFlags := c.Meta.defaultFlagSet("providers lock")
c.Meta.varFlagSet(cmdFlags)
var optPlatforms FlagStringSlice
var fsMirrorDir string
var netMirrorURL string

View File

@ -35,6 +35,7 @@ func (c *ProvidersMirrorCommand) Synopsis() string {
func (c *ProvidersMirrorCommand) Run(args []string) int {
args = c.Meta.process(args)
cmdFlags := c.Meta.defaultFlagSet("providers mirror")
c.Meta.varFlagSet(cmdFlags)
var optPlatforms FlagStringSlice
cmdFlags.Var(&optPlatforms, "platform", "target platform")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }

View File

@ -32,6 +32,7 @@ func (c *ProvidersSchemaCommand) Synopsis() string {
func (c *ProvidersSchemaCommand) Run(args []string) int {
args = c.Meta.process(args)
cmdFlags := c.Meta.defaultFlagSet("providers schema")
c.Meta.varFlagSet(cmdFlags)
var jsonOutput bool
cmdFlags.BoolVar(&jsonOutput, "json", false, "produce JSON output")

View File

@ -69,6 +69,9 @@ func (c *RefreshCommand) Run(rawArgs []string) int {
// object state for now.
c.Meta.parallelism = args.Operation.Parallelism
// Inject variables from args into meta for static evaluation
c.GatherVariables(args.Vars)
// Load the encryption configuration
enc, encDiags := c.Encryption()
diags = diags.Append(encDiags)
@ -93,13 +96,6 @@ func (c *RefreshCommand) Run(rawArgs []string) int {
return 1
}
// Collect variable value and add them to the operation request
diags = diags.Append(c.GatherVariables(opReq, args.Vars))
if diags.HasErrors() {
view.Diagnostics(diags)
return 1
}
// Before we delegate to the backend, we'll print any warning diagnostics
// we've accumulated here, since the backend will start fresh with its own
// diagnostics.
@ -107,10 +103,9 @@ func (c *RefreshCommand) Run(rawArgs []string) int {
diags = nil
// Perform the operation
op, err := c.RunOperation(be, opReq)
if err != nil {
diags = diags.Append(err)
view.Diagnostics(diags)
op, diags := c.RunOperation(be, opReq)
view.Diagnostics(diags)
if diags.HasErrors() {
return 1
}
@ -168,9 +163,7 @@ func (c *RefreshCommand) OperationRequest(be backend.Enhanced, view views.Refres
return opReq, diags
}
func (c *RefreshCommand) GatherVariables(opReq *backend.Operation, args *arguments.Vars) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
func (c *RefreshCommand) GatherVariables(args *arguments.Vars) {
// FIXME the arguments package currently trivially gathers variable related
// arguments in a heterogenous slice, in order to minimize the number of
// code paths gathering variables during the transition to this structure.
@ -185,9 +178,6 @@ func (c *RefreshCommand) GatherVariables(opReq *backend.Operation, args *argumen
items[i].Value = varArgs[i].Value
}
c.Meta.variableArgs = rawFlags{items: &items}
opReq.Variables, diags = c.collectVariableValues()
return diags
}
func (c *RefreshCommand) Help() string {

View File

@ -78,6 +78,9 @@ func (c *ShowCommand) Run(rawArgs []string) int {
return 1
}
// Inject variables from args into meta for static evaluation
c.GatherVariables(args.Vars)
// Load the encryption configuration
enc, encDiags := c.Encryption()
diags = diags.Append(encDiags)
@ -119,6 +122,23 @@ func (c *ShowCommand) Synopsis() string {
return "Show the current state or a saved plan"
}
func (c *ShowCommand) GatherVariables(args *arguments.Vars) {
// FIXME the arguments package currently trivially gathers variable related
// arguments in a heterogenous slice, in order to minimize the number of
// code paths gathering variables during the transition to this structure.
// Once all commands that gather variables have been converted to this
// structure, we could move the variable gathering code to the arguments
// package directly, removing this shim layer.
varArgs := args.All()
items := make([]rawFlag, len(varArgs))
for i := range varArgs {
items[i].Name = varArgs[i].Name
items[i].Value = varArgs[i].Value
}
c.Meta.variableArgs = rawFlags{items: &items}
}
func (c *ShowCommand) show(path string, enc encryption.Encryption) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, *tofu.Schemas, tfdiags.Diagnostics) {
var diags, showDiags, migrateDiags tfdiags.Diagnostics
var plan *plans.Plan
@ -202,11 +222,17 @@ func (c *ShowCommand) showFromPath(path string, enc encryption.Encryption) (*pla
var stateFile *statefile.File
var config *configs.Config
rootCall, callDiags := c.rootModuleCall(".")
diags = diags.Append(callDiags)
if diags.HasErrors() {
return nil, nil, nil, nil, diags
}
// Path might be a local plan file, a bookmark to a saved cloud plan, or a
// state file. First, try to get a plan and associated data from a local
// plan file. If that fails, try to get a json plan from the path argument.
// If that fails, try to get the statefile from the path argument.
plan, jsonPlan, stateFile, config, planErr = c.getPlanFromPath(path, enc)
plan, jsonPlan, stateFile, config, planErr = c.getPlanFromPath(path, enc, rootCall)
if planErr != nil {
stateFile, stateErr = getStateFromPath(path, enc)
if stateErr != nil {
@ -274,7 +300,7 @@ func (c *ShowCommand) showFromPath(path string, enc encryption.Encryption) (*pla
// yield a json plan, and cloud plans do not yield real plan/state/config
// structs. An error generally suggests that the given path is either a
// directory or a statefile.
func (c *ShowCommand) getPlanFromPath(path string, enc encryption.Encryption) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, error) {
func (c *ShowCommand) getPlanFromPath(path string, enc encryption.Encryption, rootCall configs.StaticModuleCall) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, error) {
var err error
var plan *plans.Plan
var jsonPlan *cloudplan.RemotePlanJSON
@ -287,7 +313,7 @@ func (c *ShowCommand) getPlanFromPath(path string, enc encryption.Encryption) (*
}
if lp, ok := pf.Local(); ok {
plan, stateFile, config, err = getDataFromPlanfileReader(lp)
plan, stateFile, config, err = getDataFromPlanfileReader(lp, rootCall)
} else if cp, ok := pf.Cloud(); ok {
redacted := c.viewType != arguments.ViewJSON
jsonPlan, err = c.getDataFromCloudPlan(cp, redacted, enc)
@ -316,7 +342,7 @@ func (c *ShowCommand) getDataFromCloudPlan(plan *cloudplan.SavedPlanBookmark, re
}
// getDataFromPlanfileReader returns a plan, statefile, and config, extracted from a local plan file.
func getDataFromPlanfileReader(planReader *planfile.Reader) (*plans.Plan, *statefile.File, *configs.Config, error) {
func getDataFromPlanfileReader(planReader *planfile.Reader, rootCall configs.StaticModuleCall) (*plans.Plan, *statefile.File, *configs.Config, error) {
// Get plan
plan, err := planReader.ReadPlan()
if err != nil {
@ -330,7 +356,7 @@ func getDataFromPlanfileReader(planReader *planfile.Reader) (*plans.Plan, *state
}
// Get config
config, diags := planReader.ReadConfig()
config, diags := planReader.ReadConfig(rootCall)
if diags.HasErrors() {
return nil, nil, nil, errUnusable(diags.Err(), "local plan")
}

View File

@ -27,6 +27,7 @@ func (c *StateListCommand) Run(args []string) int {
args = c.Meta.process(args)
var statePath string
cmdFlags := c.Meta.defaultFlagSet("state list")
c.Meta.varFlagSet(cmdFlags)
cmdFlags.StringVar(&statePath, "state", "", "path")
lookupId := cmdFlags.String("id", "", "Restrict output to paths with a resource having the specified ID.")
if err := cmdFlags.Parse(args); err != nil {

View File

@ -24,6 +24,7 @@ type StatePullCommand struct {
func (c *StatePullCommand) Run(args []string) int {
args = c.Meta.process(args)
cmdFlags := c.Meta.defaultFlagSet("state pull")
c.Meta.varFlagSet(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
return 1

View File

@ -32,6 +32,7 @@ type StateShowCommand struct {
func (c *StateShowCommand) Run(args []string) int {
args = c.Meta.process(args)
cmdFlags := c.Meta.defaultFlagSet("state show")
c.Meta.varFlagSet(cmdFlags)
cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
if err := cmdFlags.Parse(args); err != nil {
c.Streams.Eprintf("Error parsing command-line flags: %s\n", err.Error())

View File

@ -105,6 +105,24 @@ func (c *TestCommand) Run(rawArgs []string) int {
view := views.NewTest(args.ViewType, c.View)
// Users can also specify variables via the command line, so we'll parse
// all that here.
var items []rawFlag
for _, variable := range args.Vars.All() {
items = append(items, rawFlag{
Name: variable.Name,
Value: variable.Value,
})
}
c.variableArgs = rawFlags{items: &items}
variables, variableDiags := c.collectVariableValues()
diags = diags.Append(variableDiags)
if variableDiags.HasErrors() {
view.Diagnostics(nil, nil, diags)
return 1
}
config, configDiags := c.loadConfigWithTests(".", args.TestDirectory)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
@ -188,24 +206,6 @@ func (c *TestCommand) Run(rawArgs []string) int {
return 1
}
// Users can also specify variables via the command line, so we'll parse
// all that here.
var items []rawFlag
for _, variable := range args.Vars.All() {
items = append(items, rawFlag{
Name: variable.Name,
Value: variable.Value,
})
}
c.variableArgs = rawFlags{items: &items}
variables, variableDiags := c.collectVariableValues()
diags = diags.Append(variableDiags)
if variableDiags.HasErrors() {
view.Diagnostics(nil, nil, diags)
return 1
}
opts, err := c.contextOpts()
if err != nil {
diags = diags.Append(err)

View File

@ -0,0 +1,7 @@
variable "src" {
type = string
}
module "mod" {
source = var.src
}

View File

@ -0,0 +1,10 @@
# See internal/initwd/testdata/registry-modules/root.tf for more information on the module required
variable "modver" {
type = string
}
module "acctest_root" {
source = "hashicorp/module-installer-acctest/aws"
version = nonsensitive(var.modver)
}

View File

@ -1,7 +1,7 @@
{
"format_version": "1.0",
"valid": false,
"error_count": 4,
"error_count": 3,
"warning_count": 0,
"diagnostics": [
{
@ -58,34 +58,8 @@
},
{
"severity": "error",
"summary": "Variables not allowed",
"detail": "Variables may not be used here.",
"range": {
"filename": "testdata/validate-invalid/incorrectmodulename/main.tf",
"start": {
"line": 5,
"column": 12,
"byte": 55
},
"end": {
"line": 5,
"column": 15,
"byte": 58
}
},
"snippet": {
"context": "module \"super\"",
"code": " source = var.modulename",
"start_line": 5,
"highlight_start_offset": 11,
"highlight_end_offset": 14,
"values": []
}
},
{
"severity": "error",
"summary": "Unsuitable value type",
"detail": "Unsuitable value: value must be known",
"summary": "Undefined variable",
"detail": "Undefined variable var.modulename",
"range": {
"filename": "testdata/validate-invalid/incorrectmodulename/main.tf",
"start": {

View File

@ -28,6 +28,7 @@ func (c *UnlockCommand) Run(args []string) int {
args = c.Meta.process(args)
var force bool
cmdFlags := c.Meta.defaultFlagSet("force-unlock")
c.Meta.varFlagSet(cmdFlags)
cmdFlags.BoolVar(&force, "force", false, "force")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {

View File

@ -191,10 +191,6 @@ func TestModuleWithIncorrectNameShouldFail(t *testing.T) {
if !strings.Contains(output.Stderr(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
}
wantError = `Error: Variables not allowed`
if !strings.Contains(output.Stderr(), wantError) {
t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr())
}
}
func TestWronglyUsedInterpolationShouldFail(t *testing.T) {

View File

@ -33,6 +33,7 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int {
var stateLock bool
var stateLockTimeout time.Duration
cmdFlags := c.Meta.defaultFlagSet("workspace delete")
c.Meta.varFlagSet(cmdFlags)
cmdFlags.BoolVar(&force, "force", false, "force removal of a non-empty workspace")
cmdFlags.BoolVar(&stateLock, "lock", true, "lock state")
cmdFlags.DurationVar(&stateLockTimeout, "lock-timeout", 0, "lock timeout")

View File

@ -25,6 +25,7 @@ func (c *WorkspaceListCommand) Run(args []string) int {
envCommandShowWarning(c.Ui, c.LegacyName)
cmdFlags := c.Meta.defaultFlagSet("workspace list")
c.Meta.varFlagSet(cmdFlags)
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))

View File

@ -35,6 +35,7 @@ func (c *WorkspaceNewCommand) Run(args []string) int {
var stateLockTimeout time.Duration
var statePath string
cmdFlags := c.Meta.defaultFlagSet("workspace new")
c.Meta.varFlagSet(cmdFlags)
cmdFlags.BoolVar(&stateLock, "lock", true, "lock state")
cmdFlags.DurationVar(&stateLockTimeout, "lock-timeout", 0, "lock timeout")
cmdFlags.StringVar(&statePath, "state", "", "tofu state file")

View File

@ -26,6 +26,7 @@ func (c *WorkspaceSelectCommand) Run(args []string) int {
var orCreate bool
cmdFlags := c.Meta.defaultFlagSet("workspace select")
c.Meta.varFlagSet(cmdFlags)
cmdFlags.BoolVar(&orCreate, "or-create", false, "create workspace if it does not exist")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {

View File

@ -6,9 +6,13 @@
package configs
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/lang"
"github.com/zclconf/go-cty/cty"
)
@ -17,6 +21,7 @@ import (
type Backend struct {
Type string
Config hcl.Body
Eval *StaticEvaluator
TypeRange hcl.Range
DeclRange hcl.Range
@ -41,12 +46,18 @@ func decodeBackendBlock(block *hcl.Block) (*Backend, hcl.Diagnostics) {
// for the purpose of hashing, so that an incomplete configuration can still
// be hashed. Other errors, such as extraneous attributes, have no such special
// case.
func (b *Backend) Hash(schema *configschema.Block) int {
func (b *Backend) Hash(schema *configschema.Block) (int, hcl.Diagnostics) {
// Don't fail if required attributes are not set. Instead, we'll just
// hash them as nulls.
schema = schema.NoneRequired()
spec := schema.DecoderSpec()
val, _ := hcldec.Decode(b.Config, spec, nil)
// This is a bit of an odd workaround, but the decode below intentionally ignores
// errors. I don't want to try to change that at this point, but it may be worth doing
// at some point. For now, I'm just looking to see if there are any references that are
// not valid that the user should look at, instead of just producing an invalid backend object.
diags := b.referenceDiagnostics(schema)
val, _ := b.Decode(schema)
if val == cty.NilVal {
val = cty.UnknownVal(schema.ImpliedType())
}
@ -56,5 +67,33 @@ func (b *Backend) Hash(schema *configschema.Block) int {
val,
})
return toHash.Hash()
return toHash.Hash(), diags
}
func (b *Backend) Decode(schema *configschema.Block) (cty.Value, hcl.Diagnostics) {
return b.Eval.DecodeBlock(b.Config, schema.DecoderSpec(), StaticIdentifier{
Module: addrs.RootModule,
Subject: fmt.Sprintf("backend.%s", b.Type),
DeclRange: b.DeclRange,
})
}
// This is a hack that may not be needed, but preserves the idea that invalid backends will show a cryptic error about running init duing plan/apply startup.
func (b *Backend) referenceDiagnostics(schema *configschema.Block) hcl.Diagnostics {
var diags hcl.Diagnostics
refs, refsDiags := lang.References(addrs.ParseRef, hcldec.Variables(b.Config, schema.DecoderSpec()))
diags = append(diags, refsDiags.ToHCL()...)
if diags.HasErrors() {
return diags
}
_, ctxDiags := b.Eval.scope(StaticIdentifier{
Module: addrs.RootModule,
Subject: fmt.Sprintf("backend.%s", b.Type),
DeclRange: b.DeclRange,
}).EvalContext(refs)
diags = append(diags, ctxDiags.ToHCL()...)
return diags
}

View File

@ -13,6 +13,7 @@ import (
// or file.
type CloudConfig struct {
Config hcl.Body
eval *StaticEvaluator
DeclRange hcl.Range
}
@ -28,5 +29,6 @@ func (c *CloudConfig) ToBackendConfig() Backend {
return Backend{
Type: "cloud",
Config: c.Config,
Eval: c.eval,
}
}

View File

@ -138,10 +138,11 @@ func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config,
Name: call.Name,
Path: path,
SourceAddr: call.SourceAddr,
SourceAddrRange: call.SourceAddrRange,
SourceAddrRange: call.Source.Range(),
VersionConstraint: call.Version,
Parent: parent,
CallRange: call.DeclRange,
Call: NewStaticModuleCall(path, call.Variables, parent.Root.Module.SourceDir, call.Workspace),
}
child, modDiags := loadModule(parent.Root, &req, walker)
diags = append(diags, modDiags...)
@ -298,6 +299,10 @@ type ModuleRequest struct {
// subject of an error diagnostic that relates to the module call itself,
// rather than to either its source address or its version number.
CallRange hcl.Range
// This is where variables and other information from the calling module
// are propogated to the child module for use in the static evaluator
Call StaticModuleCall
}
// DisabledModuleWalker is a ModuleWalker that doesn't support

View File

@ -23,7 +23,7 @@ import (
func TestBuildConfig(t *testing.T) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDir("testdata/config-build")
mod, diags := parser.LoadConfigDir("testdata/config-build", RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
if mod == nil {
t.Fatal("got nil root module; want non-nil")
@ -38,10 +38,10 @@ func TestBuildConfig(t *testing.T) {
// various different source address syntaxes OpenTofu supports.
sourcePath := filepath.Join("testdata/config-build", req.SourceAddr.String())
mod, diags := parser.LoadConfigDir(sourcePath)
mod, modDiags := parser.LoadConfigDir(sourcePath, req.Call)
version, _ := version.NewVersion(fmt.Sprintf("1.0.%d", versionI))
versionI++
return mod, version, diags
return mod, version, modDiags
},
))
assertNoDiagnostics(t, diags)
@ -79,7 +79,7 @@ func TestBuildConfig(t *testing.T) {
func TestBuildConfigDiags(t *testing.T) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDir("testdata/nested-errors")
mod, diags := parser.LoadConfigDir("testdata/nested-errors", RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
if mod == nil {
t.Fatal("got nil root module; want non-nil")
@ -94,10 +94,10 @@ func TestBuildConfigDiags(t *testing.T) {
// various different source address syntaxes OpenTofu supports.
sourcePath := filepath.Join("testdata/nested-errors", req.SourceAddr.String())
mod, diags := parser.LoadConfigDir(sourcePath)
mod, modDiags := parser.LoadConfigDir(sourcePath, req.Call)
version, _ := version.NewVersion(fmt.Sprintf("1.0.%d", versionI))
versionI++
return mod, version, diags
return mod, version, modDiags
},
))
@ -124,7 +124,7 @@ func TestBuildConfigDiags(t *testing.T) {
func TestBuildConfigChildModuleBackend(t *testing.T) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDir("testdata/nested-backend-warning")
mod, diags := parser.LoadConfigDir("testdata/nested-backend-warning", RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
if mod == nil {
t.Fatal("got nil root module; want non-nil")
@ -138,9 +138,9 @@ func TestBuildConfigChildModuleBackend(t *testing.T) {
// various different source address syntaxes OpenTofu supports.
sourcePath := filepath.Join("testdata/nested-backend-warning", req.SourceAddr.String())
mod, diags := parser.LoadConfigDir(sourcePath)
mod, modDiags := parser.LoadConfigDir(sourcePath, req.Call)
version, _ := version.NewVersion("1.0.0")
return mod, version, diags
return mod, version, modDiags
},
))
@ -175,7 +175,7 @@ func TestBuildConfigInvalidModules(t *testing.T) {
parser := NewParser(nil)
path := filepath.Join(testDir, name)
mod, diags := parser.LoadConfigDirWithTests(path, "tests")
mod, diags := parser.LoadConfigDirWithTests(path, "tests", RootModuleCallForTesting())
if diags.HasErrors() {
// these tests should only trigger errors that are caught in
// the config loader.
@ -221,7 +221,7 @@ func TestBuildConfigInvalidModules(t *testing.T) {
// for simplicity, these tests will treat all source
// addresses as relative to the root module
sourcePath := filepath.Join(path, req.SourceAddr.String())
mod, diags := parser.LoadConfigDir(sourcePath)
mod, diags := parser.LoadConfigDir(sourcePath, req.Call)
version, _ := version.NewVersion("1.0.0")
return mod, version, diags
},
@ -296,7 +296,7 @@ func TestBuildConfigInvalidModules(t *testing.T) {
func TestBuildConfig_WithNestedTestModules(t *testing.T) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-tests-nested-module", "tests")
mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-tests-nested-module", "tests", RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
if mod == nil {
t.Fatal("got nil root module; want non-nil")
@ -317,9 +317,9 @@ func TestBuildConfig_WithNestedTestModules(t *testing.T) {
}
sourcePath := filepath.Join("testdata/valid-modules/with-tests-nested-module", addr)
mod, diags := parser.LoadConfigDir(sourcePath)
mod, modDiags := parser.LoadConfigDir(sourcePath, req.Call)
version, _ := version.NewVersion("1.0.0")
return mod, version, diags
return mod, version, modDiags
},
))
assertNoDiagnostics(t, diags)
@ -376,7 +376,7 @@ func TestBuildConfig_WithNestedTestModules(t *testing.T) {
func TestBuildConfig_WithTestModule(t *testing.T) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-tests-module", "tests")
mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-tests-module", "tests", RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
if mod == nil {
t.Fatal("got nil root module; want non-nil")
@ -390,9 +390,9 @@ func TestBuildConfig_WithTestModule(t *testing.T) {
// various different source address syntaxes OpenTofu supports.
sourcePath := filepath.Join("testdata/valid-modules/with-tests-module", req.SourceAddr.String())
mod, diags := parser.LoadConfigDir(sourcePath)
mod, modDiags := parser.LoadConfigDir(sourcePath, req.Call)
version, _ := version.NewVersion("1.0.0")
return mod, version, diags
return mod, version, modDiags
},
))
assertNoDiagnostics(t, diags)

View File

@ -24,14 +24,14 @@ import (
//
// LoadConfig performs the basic syntax and uniqueness validations that are
// required to process the individual modules
func (l *Loader) LoadConfig(rootDir string) (*configs.Config, hcl.Diagnostics) {
return l.loadConfig(l.parser.LoadConfigDir(rootDir))
func (l *Loader) LoadConfig(rootDir string, call configs.StaticModuleCall) (*configs.Config, hcl.Diagnostics) {
return l.loadConfig(l.parser.LoadConfigDir(rootDir, call))
}
// LoadConfigWithTests matches LoadConfig, except the configs.Config contains
// any relevant .tftest.hcl files.
func (l *Loader) LoadConfigWithTests(rootDir string, testDir string) (*configs.Config, hcl.Diagnostics) {
return l.loadConfig(l.parser.LoadConfigDirWithTests(rootDir, testDir))
func (l *Loader) LoadConfigWithTests(rootDir string, testDir string, call configs.StaticModuleCall) (*configs.Config, hcl.Diagnostics) {
return l.loadConfig(l.parser.LoadConfigDirWithTests(rootDir, testDir, call))
}
func (l *Loader) loadConfig(rootMod *configs.Module, diags hcl.Diagnostics) (*configs.Config, hcl.Diagnostics) {
@ -83,7 +83,7 @@ func (l *Loader) moduleWalkerLoad(req *configs.ModuleRequest) (*configs.Module,
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Module source has changed",
Detail: "The source address was changed since this module was installed. Run \"tofu init\" to install all modules required by this configuration.",
Detail: fmt.Sprintf("The source address was changed from %q to %q since this module was installed. Run \"tofu init\" to install all modules required by this configuration.", record.SourceAddr, req.SourceAddr.String()),
Subject: &req.SourceAddrRange,
})
}
@ -107,7 +107,7 @@ func (l *Loader) moduleWalkerLoad(req *configs.ModuleRequest) (*configs.Module,
})
}
mod, mDiags := l.parser.LoadConfigDir(record.Dir)
mod, mDiags := l.parser.LoadConfigDir(record.Dir, req.Call)
diags = append(diags, mDiags...)
if mod == nil {
// nil specifically indicates that the directory does not exist or

View File

@ -27,7 +27,7 @@ func TestLoaderLoadConfig_okay(t *testing.T) {
t.Fatalf("unexpected error from NewLoader: %s", err)
}
cfg, diags := loader.LoadConfig(fixtureDir)
cfg, diags := loader.LoadConfig(fixtureDir, configs.RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
if cfg == nil {
t.Fatalf("config is nil; want non-nil")
@ -75,7 +75,7 @@ func TestLoaderLoadConfig_addVersion(t *testing.T) {
t.Fatalf("unexpected error from NewLoader: %s", err)
}
_, diags := loader.LoadConfig(fixtureDir)
_, diags := loader.LoadConfig(fixtureDir, configs.RootModuleCallForTesting())
if !diags.HasErrors() {
t.Fatalf("success; want error")
}
@ -96,7 +96,7 @@ func TestLoaderLoadConfig_loadDiags(t *testing.T) {
t.Fatalf("unexpected error from NewLoader: %s", err)
}
cfg, diags := loader.LoadConfig(fixtureDir)
cfg, diags := loader.LoadConfig(fixtureDir, configs.RootModuleCallForTesting())
if !diags.HasErrors() {
t.Fatal("success; want error")
}
@ -120,7 +120,7 @@ func TestLoaderLoadConfig_loadDiagsFromSubmodules(t *testing.T) {
t.Fatalf("unexpected error from NewLoader: %s", err)
}
cfg, diags := loader.LoadConfig(fixtureDir)
cfg, diags := loader.LoadConfig(fixtureDir, configs.RootModuleCallForTesting())
if !diags.HasErrors() {
t.Fatalf("loading succeeded; want an error")
}
@ -170,7 +170,7 @@ func TestLoaderLoadConfig_childProviderGrandchildCount(t *testing.T) {
t.Fatalf("unexpected error from NewLoader: %s", err)
}
cfg, diags := loader.LoadConfig(fixtureDir)
cfg, diags := loader.LoadConfig(fixtureDir, configs.RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
if cfg == nil {
t.Fatalf("config is nil; want non-nil")
@ -200,7 +200,7 @@ func TestLoaderLoadConfig_childProviderGrandchildCount(t *testing.T) {
t.Fatalf("unexpected error from NewLoader: %s", err)
}
_, diags := loader.LoadConfig(fixtureDir)
_, diags := loader.LoadConfig(fixtureDir, configs.RootModuleCallForTesting())
if !diags.HasErrors() {
t.Fatalf("loading succeeded; want an error")
}

View File

@ -23,8 +23,8 @@ import (
// LoadConfigWithSnapshot is a variant of LoadConfig that also simultaneously
// creates an in-memory snapshot of the configuration files used, which can
// be later used to create a loader that may read only from this snapshot.
func (l *Loader) LoadConfigWithSnapshot(rootDir string) (*configs.Config, *Snapshot, hcl.Diagnostics) {
rootMod, diags := l.parser.LoadConfigDir(rootDir)
func (l *Loader) LoadConfigWithSnapshot(rootDir string, call configs.StaticModuleCall) (*configs.Config, *Snapshot, hcl.Diagnostics) {
rootMod, diags := l.parser.LoadConfigDir(rootDir, call)
if rootMod == nil {
return nil, nil, diags
}

View File

@ -13,6 +13,7 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/go-test/deep"
"github.com/opentofu/opentofu/internal/configs"
)
func TestLoadConfigWithSnapshot(t *testing.T) {
@ -24,7 +25,7 @@ func TestLoadConfigWithSnapshot(t *testing.T) {
t.Fatalf("unexpected error from NewLoader: %s", err)
}
_, got, diags := loader.LoadConfigWithSnapshot(fixtureDir)
_, got, diags := loader.LoadConfigWithSnapshot(fixtureDir, configs.RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
if got == nil {
t.Fatalf("snapshot is nil; want non-nil")
@ -91,9 +92,9 @@ func TestLoadConfigWithSnapshot_invalidSource(t *testing.T) {
t.Fatalf("unexpected error from NewLoader: %s", err)
}
_, _, diags := loader.LoadConfigWithSnapshot(".")
_, _, diags := loader.LoadConfigWithSnapshot(".", configs.RootModuleCallForTesting())
if !diags.HasErrors() {
t.Error("LoadConfigWithSnapshot succeeded; want errors")
t.Error("LoadConfigWithSnapshot succeeded; want errors", configs.RootModuleCallForTesting())
}
}
@ -106,7 +107,7 @@ func TestSnapshotRoundtrip(t *testing.T) {
t.Fatalf("unexpected error from NewLoader: %s", err)
}
_, snap, diags := loader.LoadConfigWithSnapshot(fixtureDir)
_, snap, diags := loader.LoadConfigWithSnapshot(fixtureDir, configs.RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
if snap == nil {
t.Fatalf("snapshot is nil; want non-nil")
@ -117,7 +118,7 @@ func TestSnapshotRoundtrip(t *testing.T) {
t.Fatalf("loader is nil; want non-nil")
}
config, diags := snapLoader.LoadConfig(fixtureDir)
config, diags := snapLoader.LoadConfig(fixtureDir, configs.RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
if config == nil {
t.Fatalf("config is nil; want non-nil")

View File

@ -40,7 +40,7 @@ func TestEscapingBlockResource(t *testing.T) {
// they only appear nested inside resource blocks.)
parser := NewParser(nil)
mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/resource")
mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/resource", RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
if mod == nil {
t.Fatal("got nil root module; want non-nil")
@ -138,7 +138,7 @@ func TestEscapingBlockResource(t *testing.T) {
func TestEscapingBlockData(t *testing.T) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/data")
mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/data", RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
if mod == nil {
t.Fatal("got nil root module; want non-nil")
@ -204,7 +204,7 @@ func TestEscapingBlockData(t *testing.T) {
func TestEscapingBlockModule(t *testing.T) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/module")
mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/module", RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
if mod == nil {
t.Fatal("got nil root module; want non-nil")
@ -270,7 +270,7 @@ func TestEscapingBlockModule(t *testing.T) {
func TestEscapingBlockProvider(t *testing.T) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/provider")
mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/provider", RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
if mod == nil {
t.Fatal("got nil root module; want non-nil")

View File

@ -29,7 +29,7 @@ func TestExperimentsConfig(t *testing.T) {
t.Run("current", func(t *testing.T) {
parser := NewParser(nil)
parser.AllowLanguageExperiments(true)
mod, diags := parser.LoadConfigDir("testdata/experiments/current")
mod, diags := parser.LoadConfigDir("testdata/experiments/current", RootModuleCallForTesting())
if got, want := len(diags), 1; got != want {
t.Fatalf("wrong number of diagnostics %d; want %d", got, want)
}
@ -57,7 +57,7 @@ func TestExperimentsConfig(t *testing.T) {
t.Run("concluded", func(t *testing.T) {
parser := NewParser(nil)
parser.AllowLanguageExperiments(true)
_, diags := parser.LoadConfigDir("testdata/experiments/concluded")
_, diags := parser.LoadConfigDir("testdata/experiments/concluded", RootModuleCallForTesting())
if got, want := len(diags), 1; got != want {
t.Fatalf("wrong number of diagnostics %d; want %d", got, want)
}
@ -79,7 +79,7 @@ func TestExperimentsConfig(t *testing.T) {
t.Run("concluded", func(t *testing.T) {
parser := NewParser(nil)
parser.AllowLanguageExperiments(true)
_, diags := parser.LoadConfigDir("testdata/experiments/unknown")
_, diags := parser.LoadConfigDir("testdata/experiments/unknown", RootModuleCallForTesting())
if got, want := len(diags), 1; got != want {
t.Fatalf("wrong number of diagnostics %d; want %d", got, want)
}
@ -101,7 +101,7 @@ func TestExperimentsConfig(t *testing.T) {
t.Run("invalid", func(t *testing.T) {
parser := NewParser(nil)
parser.AllowLanguageExperiments(true)
_, diags := parser.LoadConfigDir("testdata/experiments/invalid")
_, diags := parser.LoadConfigDir("testdata/experiments/invalid", RootModuleCallForTesting())
if got, want := len(diags), 1; got != want {
t.Fatalf("wrong number of diagnostics %d; want %d", got, want)
}
@ -123,7 +123,7 @@ func TestExperimentsConfig(t *testing.T) {
t.Run("disallowed", func(t *testing.T) {
parser := NewParser(nil)
parser.AllowLanguageExperiments(false) // The default situation for release builds
_, diags := parser.LoadConfigDir("testdata/experiments/current")
_, diags := parser.LoadConfigDir("testdata/experiments/current", RootModuleCallForTesting())
if got, want := len(diags), 1; got != want {
t.Fatalf("wrong number of diagnostics %d; want %d", got, want)
}

View File

@ -107,8 +107,8 @@ type File struct {
// NewModuleWithTests matches NewModule except it will also load in the provided
// test files.
func NewModuleWithTests(primaryFiles, overrideFiles []*File, testFiles map[string]*TestFile) (*Module, hcl.Diagnostics) {
mod, diags := NewModule(primaryFiles, overrideFiles)
func NewModuleWithTests(primaryFiles, overrideFiles []*File, testFiles map[string]*TestFile, call StaticModuleCall, sourceDir string) (*Module, hcl.Diagnostics) {
mod, diags := NewModule(primaryFiles, overrideFiles, call, sourceDir)
if mod != nil {
mod.Tests = testFiles
}
@ -123,7 +123,7 @@ func NewModuleWithTests(primaryFiles, overrideFiles []*File, testFiles map[strin
// will be incomplete and error diagnostics will be returned. Careful static
// analysis of the returned Module is still possible in this case, but the
// module will probably not be semantically valid.
func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
func NewModule(primaryFiles, overrideFiles []*File, call StaticModuleCall, sourceDir string) (*Module, hcl.Diagnostics) {
var diags hcl.Diagnostics
mod := &Module{
ProviderConfigs: map[string]*Provider{},
@ -137,6 +137,7 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
Checks: map[string]*Check{},
ProviderMetas: map[addrs.Provider]*ProviderMeta{},
Tests: map[string]*TestFile{},
SourceDir: sourceDir,
}
// Process the required_providers blocks first, to ensure that all
@ -184,6 +185,24 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
diags = append(diags, fileDiags...)
}
// Static evaluation to build a StaticContext now that module has all relevant Locals / Variables
eval := NewStaticEvaluator(mod, call)
// If we have a backend, it may have fields that require locals/vars
if mod.Backend != nil {
// We don't know the backend type / loader at this point so we save the context for later use
mod.Backend.Eval = eval
}
if mod.CloudConfig != nil {
mod.CloudConfig.eval = eval
}
// Process all module calls now that we have the static context
for _, mc := range mod.ModuleCalls {
mDiags := mc.decodeStaticFields(eval)
diags = append(diags, mDiags...)
}
diags = append(diags, checkModuleExperiments(mod)...)
// Generate the FQN -> LocalProviderName map

View File

@ -6,11 +6,12 @@
package configs
import (
"errors"
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/getmodules"
@ -20,14 +21,19 @@ import (
type ModuleCall struct {
Name string
SourceAddr addrs.ModuleSource
SourceAddrRaw string
SourceAddrRange hcl.Range
SourceSet bool
Source hcl.Expression
SourceAddrRaw string
SourceAddr addrs.ModuleSource
SourceSet bool
// Used when building the corresponding StaticModuleCall
Variables StaticModuleVariables
Workspace string
Config hcl.Body
Version VersionConstraint
VersionAttr *hcl.Attribute
Version VersionConstraint
Count hcl.Expression
ForEach hcl.Expression
@ -43,8 +49,8 @@ func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagno
var diags hcl.Diagnostics
mc := &ModuleCall{
Name: block.Labels[0],
DeclRange: block.DefRange,
Name: block.Labels[0],
}
schema := moduleBlockSchema
@ -65,75 +71,13 @@ func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagno
})
}
haveVersionArg := false
if attr, exists := content.Attributes["version"]; exists {
var versionDiags hcl.Diagnostics
mc.Version, versionDiags = decodeVersionConstraint(attr)
diags = append(diags, versionDiags...)
haveVersionArg = true
mc.VersionAttr = attr
}
if attr, exists := content.Attributes["source"]; exists {
mc.SourceSet = true
mc.SourceAddrRange = attr.Expr.Range()
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &mc.SourceAddrRaw)
diags = append(diags, valDiags...)
if !valDiags.HasErrors() {
var addr addrs.ModuleSource
var err error
if haveVersionArg {
addr, err = addrs.ParseModuleSourceRegistry(mc.SourceAddrRaw)
} else {
addr, err = addrs.ParseModuleSource(mc.SourceAddrRaw)
}
mc.SourceAddr = addr
if err != nil {
// NOTE: We leave mc.SourceAddr as nil for any situation where the
// source attribute is invalid, so any code which tries to carefully
// use the partial result of a failed config decode must be
// resilient to that.
mc.SourceAddr = nil
// NOTE: In practice it's actually very unlikely to end up here,
// because our source address parser can turn just about any string
// into some sort of remote package address, and so for most errors
// we'll detect them only during module installation. There are
// still a _few_ purely-syntax errors we can catch at parsing time,
// though, mostly related to remote package sub-paths and local
// paths.
switch err := err.(type) {
case *getmodules.MaybeRelativePathErr:
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module source address",
Detail: fmt.Sprintf(
"OpenTofu failed to determine your intended installation method for remote module package %q.\n\nIf you intended this as a path relative to the current module, use \"./%s\" instead. The \"./\" prefix indicates that the address is a relative filesystem path.",
err.Addr, err.Addr,
),
Subject: mc.SourceAddrRange.Ptr(),
})
default:
if haveVersionArg {
// In this case we'll include some extra context that
// we assumed a registry source address due to the
// version argument.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid registry module source address",
Detail: fmt.Sprintf("Failed to parse module registry address: %s.\n\nOpenTofu assumed that you intended a module registry source address because you also set the argument \"version\", which applies only to registry modules.", err),
Subject: mc.SourceAddrRange.Ptr(),
})
} else {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module source address",
Detail: fmt.Sprintf("Failed to parse module source address: %s.", err),
Subject: mc.SourceAddrRange.Ptr(),
})
}
}
}
}
mc.Source = attr.Expr
}
if attr, exists := content.Attributes["count"]; exists {
@ -202,6 +146,132 @@ func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagno
return mc, diags
}
func (mc *ModuleCall) decodeStaticFields(eval *StaticEvaluator) hcl.Diagnostics {
mc.Workspace = eval.call.workspace
mc.decodeStaticVariables(eval)
var diags hcl.Diagnostics
diags = diags.Extend(mc.decodeStaticSource(eval))
diags = diags.Extend(mc.decodeStaticVersion(eval))
return diags
}
func (mc *ModuleCall) decodeStaticSource(eval *StaticEvaluator) hcl.Diagnostics {
if mc.Source == nil {
// This is an invalid module. We already have error handling that has more context and can produce better errors in this
// scenario. Follow the trail of mc.SourceAddr -> req.SourceAddr through the command package.
return nil
}
// Decode source field
diags := eval.DecodeExpression(mc.Source, StaticIdentifier{Module: eval.call.addr, Subject: fmt.Sprintf("module.%s.source", mc.Name), DeclRange: mc.Source.Range()}, &mc.SourceAddrRaw)
//nolint:nestif // Keeping this similar to the original decode logic for easy review
if !diags.HasErrors() {
// NOTE: This code was originally executed as part of decodeModuleBlock and is now deferred until we have the config merged and static context built
var err error
if mc.VersionAttr != nil {
mc.SourceAddr, err = addrs.ParseModuleSourceRegistry(mc.SourceAddrRaw)
} else {
mc.SourceAddr, err = addrs.ParseModuleSource(mc.SourceAddrRaw)
}
if err != nil {
// NOTE: We leave SourceAddr as nil for any situation where the
// source attribute is invalid, so any code which tries to carefully
// use the partial result of a failed config decode must be
// resilient to that.
mc.SourceAddr = nil
// NOTE: In practice it's actually very unlikely to end up here,
// because our source address parser can turn just about any string
// into some sort of remote package address, and so for most errors
// we'll detect them only during module installation. There are
// still a _few_ purely-syntax errors we can catch at parsing time,
// though, mostly related to remote package sub-paths and local
// paths.
var pathErr *getmodules.MaybeRelativePathErr
if errors.As(err, &pathErr) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module source address",
Detail: fmt.Sprintf(
"OpenTofu failed to determine your intended installation method for remote module package %q.\n\nIf you intended this as a path relative to the current module, use \"./%s\" instead. The \"./\" prefix indicates that the address is a relative filesystem path.",
pathErr.Addr, pathErr.Addr,
),
Subject: mc.Source.Range().Ptr(),
})
} else {
if mc.VersionAttr != nil {
// In this case we'll include some extra context that
// we assumed a registry source address due to the
// version argument.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid registry module source address",
Detail: fmt.Sprintf("Failed to parse module registry address: %s.\n\nOpenTofu assumed that you intended a module registry source address because you also set the argument \"version\", which applies only to registry modules.", err),
Subject: mc.Source.Range().Ptr(),
})
} else {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module source address",
Detail: fmt.Sprintf("Failed to parse module source address: %s.", err),
Subject: mc.Source.Range().Ptr(),
})
}
}
}
}
return diags
}
func (mc *ModuleCall) decodeStaticVersion(eval *StaticEvaluator) hcl.Diagnostics {
var diags hcl.Diagnostics
if mc.VersionAttr == nil {
return diags
}
val, valDiags := eval.Evaluate(mc.VersionAttr.Expr, StaticIdentifier{
Module: eval.call.addr,
Subject: fmt.Sprintf("module.%s.version", mc.Name),
DeclRange: mc.VersionAttr.Range,
})
diags = diags.Extend(valDiags)
if diags.HasErrors() {
return diags
}
var verDiags hcl.Diagnostics
mc.Version, verDiags = decodeVersionConstraintValue(mc.VersionAttr, val)
return diags.Extend(verDiags)
}
func (mc *ModuleCall) decodeStaticVariables(eval *StaticEvaluator) {
attr, _ := mc.Config.JustAttributes()
mc.Variables = func(variable *Variable) (cty.Value, hcl.Diagnostics) {
v, ok := attr[variable.Name]
if !ok {
if variable.Required() {
return cty.NilVal, hcl.Diagnostics{&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing required variable in module call",
Subject: mc.Config.MissingItemRange().Ptr(),
}}
}
return variable.Default, nil
}
ident := StaticIdentifier{
Module: eval.call.addr.Child(mc.Name),
Subject: fmt.Sprintf("var.%s", variable.Name),
DeclRange: v.Range,
}
return eval.Evaluate(v.Expr, ident)
}
}
// EntersNewPackage returns true if this call is to an external module, either
// directly via a remote source address or indirectly via a registry source
// address.

View File

@ -37,11 +37,6 @@ func TestLoadModuleCall(t *testing.T) {
SourceAddr: addrs.ModuleSourceLocal("./foo"),
SourceAddrRaw: "./foo",
SourceSet: true,
SourceAddrRange: hcl.Range{
Filename: "module-calls.tf",
Start: hcl.Pos{Line: 3, Column: 12, Byte: 27},
End: hcl.Pos{Line: 3, Column: 19, Byte: 34},
},
DeclRange: hcl.Range{
Filename: "module-calls.tf",
Start: hcl.Pos{Line: 2, Column: 1, Byte: 1},
@ -60,11 +55,6 @@ func TestLoadModuleCall(t *testing.T) {
},
SourceAddrRaw: "hashicorp/bar/aws",
SourceSet: true,
SourceAddrRange: hcl.Range{
Filename: "module-calls.tf",
Start: hcl.Pos{Line: 8, Column: 12, Byte: 113},
End: hcl.Pos{Line: 8, Column: 31, Byte: 132},
},
DeclRange: hcl.Range{
Filename: "module-calls.tf",
Start: hcl.Pos{Line: 7, Column: 1, Byte: 87},
@ -78,11 +68,6 @@ func TestLoadModuleCall(t *testing.T) {
},
SourceAddrRaw: "git::https://example.com/",
SourceSet: true,
SourceAddrRange: hcl.Range{
Filename: "module-calls.tf",
Start: hcl.Pos{Line: 15, Column: 12, Byte: 193},
End: hcl.Pos{Line: 15, Column: 39, Byte: 220},
},
DependsOn: []hcl.Traversal{
{
hcl.TraverseRoot{
@ -141,6 +126,15 @@ func TestLoadModuleCall(t *testing.T) {
// here anyway... the point of this test is to ensure we handle everything
// else properly.
for _, m := range gotModules {
// This is a structural issue which existed before static evaluation, but has been made worse by it
// See https://github.com/opentofu/opentofu/issues/1467 for more details
eval := NewStaticEvaluator(nil, RootModuleCallForTesting())
diags := m.decodeStaticFields(eval)
if diags.HasErrors() {
t.Fatal(diags.Error())
}
m.Source = nil
m.Config = nil
m.Count = nil
m.ForEach = nil

View File

@ -169,9 +169,7 @@ func (mc *ModuleCall) merge(omc *ModuleCall) hcl.Diagnostics {
var diags hcl.Diagnostics
if omc.SourceSet {
mc.SourceAddr = omc.SourceAddr
mc.SourceAddrRaw = omc.SourceAddrRaw
mc.SourceAddrRange = omc.SourceAddrRange
mc.Source = omc.Source
mc.SourceSet = omc.SourceSet
}
@ -183,8 +181,8 @@ func (mc *ModuleCall) merge(omc *ModuleCall) hcl.Diagnostics {
mc.ForEach = omc.ForEach
}
if len(omc.Version.Required) != 0 {
mc.Version = omc.Version
if omc.VersionAttr != nil {
mc.VersionAttr = omc.VersionAttr
}
mc.Config = MergeBodies(mc.Config, omc.Config)

View File

@ -96,20 +96,7 @@ func TestModuleOverrideModule(t *testing.T) {
Name: "example",
SourceAddr: addrs.ModuleSourceLocal("./example2-a_override"),
SourceAddrRaw: "./example2-a_override",
SourceAddrRange: hcl.Range{
Filename: filepath.FromSlash("testdata/valid-modules/override-module/a_override.tf"),
Start: hcl.Pos{
Line: 3,
Column: 12,
Byte: 31,
},
End: hcl.Pos{
Line: 3,
Column: 35,
Byte: 54,
},
},
SourceSet: true,
SourceSet: true,
DeclRange: hcl.Range{
Filename: filepath.FromSlash("testdata/valid-modules/override-module/primary.tf"),
Start: hcl.Pos{
@ -175,6 +162,8 @@ func TestModuleOverrideModule(t *testing.T) {
// is not a useful way to assert on that.
gotConfig := got.Config
got.Config = nil
got.Source = nil
got.Variables = nil
assertResultDeepEqual(t, got, want)

View File

@ -176,7 +176,7 @@ func TestMovedBlock_decode(t *testing.T) {
func TestMovedBlock_inModule(t *testing.T) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDir("testdata/valid-modules/moved-blocks")
mod, diags := parser.LoadConfigDir("testdata/valid-modules/moved-blocks", RootModuleCallForTesting())
if diags.HasErrors() {
t.Errorf("unexpected error: %s", diags.Error())
}

View File

@ -51,7 +51,7 @@ const (
//
// .tf files are parsed using the HCL native syntax while .tf.json files are
// parsed using the HCL JSON syntax.
func (p *Parser) LoadConfigDir(path string) (*Module, hcl.Diagnostics) {
func (p *Parser) LoadConfigDir(path string, call StaticModuleCall) (*Module, hcl.Diagnostics) {
primaryPaths, overridePaths, _, diags := p.dirFiles(path, "")
if diags.HasErrors() {
return nil, diags
@ -62,17 +62,15 @@ func (p *Parser) LoadConfigDir(path string) (*Module, hcl.Diagnostics) {
override, fDiags := p.loadFiles(overridePaths, true)
diags = append(diags, fDiags...)
mod, modDiags := NewModule(primary, override)
mod, modDiags := NewModule(primary, override, call, path)
diags = append(diags, modDiags...)
mod.SourceDir = path
return mod, diags
}
// LoadConfigDirWithTests matches LoadConfigDir, but the return Module also
// contains any relevant .tftest.hcl files.
func (p *Parser) LoadConfigDirWithTests(path string, testDirectory string) (*Module, hcl.Diagnostics) {
func (p *Parser) LoadConfigDirWithTests(path string, testDirectory string, call StaticModuleCall) (*Module, hcl.Diagnostics) {
primaryPaths, overridePaths, testPaths, diags := p.dirFiles(path, testDirectory)
if diags.HasErrors() {
return nil, diags
@ -85,11 +83,9 @@ func (p *Parser) LoadConfigDirWithTests(path string, testDirectory string) (*Mod
tests, fDiags := p.loadTestFiles(path, testPaths)
diags = append(diags, fDiags...)
mod, modDiags := NewModuleWithTests(primary, override, tests)
mod, modDiags := NewModuleWithTests(primary, override, tests, call, path)
diags = append(diags, modDiags...)
mod.SourceDir = path
return mod, diags
}

View File

@ -12,6 +12,8 @@ import (
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/zclconf/go-cty/cty"
)
// TestParseLoadConfigDirSuccess is a simple test that just verifies that
@ -37,7 +39,7 @@ func TestParserLoadConfigDirSuccess(t *testing.T) {
parser := NewParser(nil)
path := filepath.Join("testdata/valid-modules", name)
mod, diags := parser.LoadConfigDir(path)
mod, diags := parser.LoadConfigDir(path, RootModuleCallForTesting())
if len(diags) != 0 && len(mod.ActiveExperiments) != 0 {
// As a special case to reduce churn while we're working
// through experimental features, we'll ignore the warning
@ -103,7 +105,14 @@ func TestParserLoadConfigDirSuccess(t *testing.T) {
"mod/" + name: string(src),
})
_, diags := parser.LoadConfigDir("mod")
_, diags := parser.LoadConfigDir("mod", NewStaticModuleCall(addrs.RootModule,
func(v *Variable) (cty.Value, hcl.Diagnostics) {
if !v.Required() {
// Allow defaults in this test
return v.Default, nil
}
panic("Variables not configured for this test!")
}, "<testing>", ""))
if diags.HasErrors() {
t.Errorf("unexpected error diagnostics")
for _, diag := range diags {
@ -133,7 +142,7 @@ func TestParserLoadConfigDirWithTests(t *testing.T) {
}
parser := NewParser(nil)
mod, diags := parser.LoadConfigDirWithTests(directory, testDirectory)
mod, diags := parser.LoadConfigDirWithTests(directory, testDirectory, RootModuleCallForTesting())
if len(diags) > 0 { // We don't want any warnings or errors.
t.Errorf("unexpected diagnostics")
for _, diag := range diags {
@ -150,7 +159,7 @@ func TestParserLoadConfigDirWithTests(t *testing.T) {
func TestParserLoadConfigDirWithTests_ReturnsWarnings(t *testing.T) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-tests", "not_real")
mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-tests", "not_real", RootModuleCallForTesting())
if len(diags) != 1 {
t.Errorf("expected exactly 1 diagnostic, but found %d", len(diags))
} else {
@ -197,7 +206,7 @@ func TestParserLoadConfigDirFailure(t *testing.T) {
parser := NewParser(nil)
path := filepath.Join("testdata/invalid-modules", name)
_, diags := parser.LoadConfigDir(path)
_, diags := parser.LoadConfigDir(path, RootModuleCallForTesting())
if !diags.HasErrors() {
t.Errorf("no errors; want at least one")
for _, diag := range diags {
@ -226,7 +235,7 @@ func TestParserLoadConfigDirFailure(t *testing.T) {
"mod/" + name: string(src),
})
_, diags := parser.LoadConfigDir("mod")
_, diags := parser.LoadConfigDir("mod", RootModuleCallForTesting())
if !diags.HasErrors() {
t.Errorf("no errors; want at least one")
for _, diag := range diags {

View File

@ -272,7 +272,15 @@ func TestParserLoadConfigFileError(t *testing.T) {
name: string(src),
})
_, diags := parser.LoadConfigFile(name)
file, diags := parser.LoadConfigFile(name)
// TODO many of these errors are now deferred until module loading
// This is a structural issue which existed before static evaluation, but has been made worse by it
// See https://github.com/opentofu/opentofu/issues/1467 for more details
eval := NewStaticEvaluator(nil, RootModuleCallForTesting())
for _, mc := range file.ModuleCalls {
mDiags := mc.decodeStaticFields(eval)
diags = append(diags, mDiags...)
}
gotErrors := make(map[int]string)
for _, diag := range diags {

View File

@ -48,7 +48,7 @@ func testParser(files map[string]string) *Parser {
func testModuleConfigFromFile(filename string) (*Config, hcl.Diagnostics) {
parser := NewParser(nil)
f, diags := parser.LoadConfigFile(filename)
mod, modDiags := NewModule([]*File{f}, nil)
mod, modDiags := NewModule([]*File{f}, nil, RootModuleCallForTesting(), filename)
diags = append(diags, modDiags...)
cfg, moreDiags := BuildConfig(mod, nil)
return cfg, append(diags, moreDiags...)
@ -58,14 +58,14 @@ func testModuleConfigFromFile(filename string) (*Config, hcl.Diagnostics) {
// a module and returns it. This is a helper for use in unit tests.
func testModuleFromDir(path string) (*Module, hcl.Diagnostics) {
parser := NewParser(nil)
return parser.LoadConfigDir(path)
return parser.LoadConfigDir(path, RootModuleCallForTesting())
}
// testModuleFromDir reads configuration from the given directory path as a
// module and returns its configuration. This is a helper for use in unit tests.
func testModuleConfigFromDir(path string) (*Config, hcl.Diagnostics) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDir(path)
mod, diags := parser.LoadConfigDir(path, RootModuleCallForTesting())
cfg, moreDiags := BuildConfig(mod, nil)
return cfg, append(diags, moreDiags...)
}
@ -76,7 +76,7 @@ func testNestedModuleConfigFromDirWithTests(t *testing.T, path string) (*Config,
t.Helper()
parser := NewParser(nil)
mod, diags := parser.LoadConfigDirWithTests(path, "tests")
mod, diags := parser.LoadConfigDirWithTests(path, "tests", RootModuleCallForTesting())
if mod == nil {
t.Fatal("got nil root module; want non-nil")
}
@ -94,7 +94,7 @@ func testNestedModuleConfigFromDir(t *testing.T, path string) (*Config, hcl.Diag
t.Helper()
parser := NewParser(nil)
mod, diags := parser.LoadConfigDir(path)
mod, diags := parser.LoadConfigDir(path, RootModuleCallForTesting())
if mod == nil {
t.Fatal("got nil root module; want non-nil")
}
@ -123,7 +123,7 @@ func buildNestedModuleConfig(mod *Module, path string, parser *Parser) (*Config,
paths = append([]string{path}, paths...)
sourcePath := filepath.Join(paths...)
mod, diags := parser.LoadConfigDir(sourcePath)
mod, diags := parser.LoadConfigDir(sourcePath, RootModuleCallForTesting())
version, _ := version.NewVersion(fmt.Sprintf("1.0.%d", versionI))
versionI++
return mod, version, diags

View File

@ -256,13 +256,9 @@ func validateProviderConfigsForTests(cfg *Config) (diags hcl.Diagnostics) {
// Let's make a little fake module call that we can use to call
// into validateProviderConfigs.
mc := &ModuleCall{
Name: run.Name,
SourceAddr: run.Module.Source,
SourceAddrRange: run.Module.SourceDeclRange,
SourceSet: true,
Version: run.Module.Version,
Providers: providers,
DeclRange: run.Module.DeclRange,
Name: run.Name,
Providers: providers,
DeclRange: run.Module.DeclRange,
}
diags = append(diags, validateProviderConfigs(mc, run.ConfigUnderTest, nil)...)

View File

@ -161,7 +161,7 @@ func TestRemovedBlock_decode(t *testing.T) {
func TestRemovedBlock_inModule(t *testing.T) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDir("testdata/valid-modules/removed-blocks")
mod, diags := parser.LoadConfigDir("testdata/valid-modules/removed-blocks", RootModuleCallForTesting())
if diags.HasErrors() {
t.Errorf("unexpected error: %s", diags.Error())
}

View File

@ -0,0 +1,119 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package configs
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/lang"
"github.com/zclconf/go-cty/cty"
)
// StaticIdentifier holds a Referencable item and where it was declared
type StaticIdentifier struct {
Module addrs.Module
Subject string
DeclRange hcl.Range
}
func (ref StaticIdentifier) String() string {
val := ref.Subject
if len(ref.Module) != 0 {
val = ref.Module.String() + ":" + val
}
return val
}
type StaticModuleVariables func(v *Variable) (cty.Value, hcl.Diagnostics)
// StaticModuleCall contains the information required to call a given module
type StaticModuleCall struct {
addr addrs.Module
vars StaticModuleVariables
rootPath string
workspace string
}
func NewStaticModuleCall(addr addrs.Module, vars StaticModuleVariables, rootPath string, workspace string) StaticModuleCall {
return StaticModuleCall{
addr: addr,
vars: vars,
rootPath: rootPath,
workspace: workspace,
}
}
// only used in testing
func RootModuleCallForTesting() StaticModuleCall {
return NewStaticModuleCall(addrs.RootModule, func(_ *Variable) (cty.Value, hcl.Diagnostics) {
panic("Variables have not been configured for this test!")
}, "<testing>", "")
}
// A static evaluator contains the information required to build a EvalContext
// which only understands "static" (non-state) data. Internally, it relies
// on staticData
type StaticEvaluator struct {
call StaticModuleCall
cfg *Module
}
// Creates a static evaluator based from the given module and module call
func NewStaticEvaluator(mod *Module, call StaticModuleCall) *StaticEvaluator {
return &StaticEvaluator{
call: call,
cfg: mod,
}
}
func (s *StaticEvaluator) scope(ident StaticIdentifier) *lang.Scope {
return newStaticScope(s, ident)
}
func (s StaticEvaluator) Evaluate(expr hcl.Expression, ident StaticIdentifier) (cty.Value, hcl.Diagnostics) {
val, diags := s.scope(ident).EvalExpr(expr, cty.DynamicPseudoType)
return val, diags.ToHCL()
}
func (s StaticEvaluator) DecodeExpression(expr hcl.Expression, ident StaticIdentifier, val any) hcl.Diagnostics {
var diags hcl.Diagnostics
refs, refsDiags := lang.ReferencesInExpr(addrs.ParseRef, expr)
diags = append(diags, refsDiags.ToHCL()...)
if diags.HasErrors() {
return diags
}
ctx, ctxDiags := s.scope(ident).EvalContext(refs)
diags = append(diags, ctxDiags.ToHCL()...)
if diags.HasErrors() {
return diags
}
return gohcl.DecodeExpression(expr, ctx, val)
}
func (s StaticEvaluator) DecodeBlock(body hcl.Body, spec hcldec.Spec, ident StaticIdentifier) (cty.Value, hcl.Diagnostics) {
var diags hcl.Diagnostics
refs, refsDiags := lang.References(addrs.ParseRef, hcldec.Variables(body, spec))
diags = append(diags, refsDiags.ToHCL()...)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
ctx, ctxDiags := s.scope(ident).EvalContext(refs)
diags = append(diags, ctxDiags.ToHCL()...)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
val, valDiags := hcldec.Decode(body, spec, ctx)
diags = append(diags, valDiags...)
return val, diags
}

View File

@ -0,0 +1,397 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package configs
import (
"fmt"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/zclconf/go-cty/cty"
)
// This exercises most of the logic in StaticEvaluator and staticScopeData
//
//nolint:gocognit,cyclop // it's a test
func TestStaticEvaluator_Evaluate(t *testing.T) {
// Synthetic file for building test components
testData := `
variable "str" {
type = string
}
variable "str_map" {
type = map(string)
}
variable "str_def" {
type = string
default = "sane default"
}
variable "str_map_def" {
type = map(string)
default = {
keyA = "A"
}
}
# Should/can variable checks be performed during static evaluation?
locals {
# Simple static cases
static = "static"
static_ref = local.static
static_fn = md5(local.static)
path_root = path.root
path_module = path.module
# Variable References with Defaults
var_str_def_ref = var.str_def
var_map_def_access = var.str_map_def["keyA"]
# Variable References without Defaults
var_str_ref = var.str
var_map_access = var.str_map["keyA"]
# Bad References
invalid_ref = invalid.attribute
unavailable_ref = foo.bar.attribute
# Circular References
circular = local.circular
circular_ref = local.circular
circular_a = local.circular_b
circular_b = local.circular_a
# Dependency chain
ref_a = var.str
ref_b = local.ref_a
ref_c = local.ref_b
# Missing References
local_missing = local.missing
var_missing = var.missing
# Terraform
ws = terraform.workspace
# Functions
func = md5("my-string")
missing_func = missing_fn("my-string")
provider_func = provider::type::fn("my-string")
}
resource "foo" "bar" {}
`
parser := testParser(map[string]string{"eval.tf": testData})
file, fileDiags := parser.LoadConfigFile("eval.tf")
if fileDiags.HasErrors() {
t.Fatal(fileDiags)
}
dummyIdentifier := StaticIdentifier{Subject: "local.test"}
t.Run("Empty Eval", func(t *testing.T) {
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir")
emptyEval := StaticEvaluator{}
// Expr with no traversals shouldn't access any fields
value, diags := emptyEval.Evaluate(mod.Locals["static"].Expr, dummyIdentifier)
if diags.HasErrors() {
t.Error(diags)
}
if value.AsString() != "static" {
t.Errorf("Expected %s got %s", "static", value.AsString())
}
// Expr with traversals should panic, indicating a programming error
defer func() {
r := recover()
if r == nil {
t.Fatalf("should panic")
}
}()
_, _ = emptyEval.Evaluate(mod.Locals["static_ref"].Expr, dummyIdentifier)
})
t.Run("Simple static cases", func(t *testing.T) {
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir")
eval := NewStaticEvaluator(mod, RootModuleCallForTesting())
locals := []struct {
ident string
value string
}{
{"static", "static"},
{"static_ref", "static"},
{"static_fn", "a81259cef8e959c624df1d456e5d3297"},
{"path_root", "<testing>"},
{"path_module", "dir"},
}
for _, local := range locals {
t.Run(local.ident, func(t *testing.T) {
value, diags := eval.Evaluate(mod.Locals[local.ident].Expr, dummyIdentifier)
if diags.HasErrors() {
t.Error(diags)
}
if value.AsString() != local.value {
t.Errorf("Expected %s got %s", local.value, value.AsString())
}
})
}
})
t.Run("Valid Variables", func(t *testing.T) {
input := map[string]cty.Value{
"str": cty.StringVal("vara"),
"str_map": cty.MapVal(map[string]cty.Value{"keyA": cty.StringVal("mapa")}),
}
call := NewStaticModuleCall(nil, func(v *Variable) (cty.Value, hcl.Diagnostics) {
if in, ok := input[v.Name]; ok {
return in, nil
}
return v.Default, nil
}, "<testing>", "")
mod, _ := NewModule([]*File{file}, nil, call, "dir")
eval := NewStaticEvaluator(mod, call)
locals := []struct {
ident string
value string
}{
{"var_str_def_ref", "sane default"},
{"var_map_def_access", "A"},
{"var_str_ref", "vara"},
{"var_map_access", "mapa"},
}
for _, local := range locals {
t.Run(local.ident, func(t *testing.T) {
value, diags := eval.Evaluate(mod.Locals[local.ident].Expr, dummyIdentifier)
if diags.HasErrors() {
t.Error(diags)
}
if value.AsString() != local.value {
t.Errorf("Expected %s got %s", local.value, value.AsString())
}
})
}
})
t.Run("Bad References", func(t *testing.T) {
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir")
eval := NewStaticEvaluator(mod, RootModuleCallForTesting())
locals := []struct {
ident string
diag string
}{
{"invalid_ref", "eval.tf:37,16-33: Dynamic value in static context; Unable to use invalid.attribute in static context, which is required by local.invalid_ref"},
{"unavailable_ref", "eval.tf:38,20-27: Dynamic value in static context; Unable to use foo.bar in static context, which is required by local.unavailable_ref"},
}
for _, local := range locals {
t.Run(local.ident, func(t *testing.T) {
badref := mod.Locals[local.ident]
_, diags := eval.Evaluate(badref.Expr, StaticIdentifier{Subject: fmt.Sprintf("local.%s", badref.Name), DeclRange: badref.DeclRange})
assertExactDiagnostics(t, diags, []string{local.diag})
})
}
})
t.Run("Circular References", func(t *testing.T) {
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir")
eval := NewStaticEvaluator(mod, RootModuleCallForTesting())
locals := []struct {
ident string
diags []string
}{
{"circular", []string{
"eval.tf:41,2-27: Circular reference; local.circular is self referential",
}},
{"circular_ref", []string{
"eval.tf:41,2-27: Circular reference; local.circular is self referential",
"eval.tf:42,2-31: Unable to compute static value; local.circular_ref depends on local.circular which is not available",
}},
{"circular_a", []string{
"eval.tf:43,2-31: Unable to compute static value; local.circular_a depends on local.circular_b which is not available",
"eval.tf:43,2-31: Circular reference; local.circular_a is self referential",
}},
}
for _, local := range locals {
t.Run(local.ident, func(t *testing.T) {
badref := mod.Locals[local.ident]
_, diags := eval.Evaluate(badref.Expr, StaticIdentifier{Subject: fmt.Sprintf("local.%s", badref.Name), DeclRange: badref.DeclRange})
assertExactDiagnostics(t, diags, local.diags)
})
}
})
t.Run("Dependency chain", func(t *testing.T) {
call := NewStaticModuleCall(nil, func(v *Variable) (cty.Value, hcl.Diagnostics) {
return cty.DynamicVal, hcl.Diagnostics{&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Variable value not provided",
Detail: fmt.Sprintf("var.%s not included", v.Name),
Subject: v.DeclRange.Ptr(),
}}
}, "<testing>", "")
mod, _ := NewModule([]*File{file}, nil, call, "dir")
eval := NewStaticEvaluator(mod, call)
badref := mod.Locals["ref_c"]
_, diags := eval.Evaluate(badref.Expr, StaticIdentifier{Subject: fmt.Sprintf("local.%s", badref.Name), DeclRange: badref.DeclRange})
assertExactDiagnostics(t, diags, []string{
"eval.tf:2,1-15: Variable value not provided; var.str not included",
"eval.tf:47,2-17: Unable to compute static value; local.ref_a depends on var.str which is not available",
"eval.tf:48,2-21: Unable to compute static value; local.ref_b depends on local.ref_a which is not available",
"eval.tf:49,2-21: Unable to compute static value; local.ref_c depends on local.ref_b which is not available",
})
})
t.Run("Missing References", func(t *testing.T) {
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir")
eval := NewStaticEvaluator(mod, RootModuleCallForTesting())
locals := []struct {
ident string
diag string
}{
{"local_missing", "eval.tf:52,18-31: Undefined local; Undefined local local.missing"},
{"var_missing", "eval.tf:53,16-27: Undefined variable; Undefined variable var.missing"},
}
for _, local := range locals {
t.Run(local.ident, func(t *testing.T) {
badref := mod.Locals[local.ident]
_, diags := eval.Evaluate(badref.Expr, StaticIdentifier{Subject: fmt.Sprintf("local.%s", badref.Name), DeclRange: badref.DeclRange})
assertExactDiagnostics(t, diags, []string{local.diag})
})
}
})
t.Run("Workspace", func(t *testing.T) {
call := NewStaticModuleCall(nil, nil, "<testing>", "my-workspace")
mod, _ := NewModule([]*File{file}, nil, call, "dir")
eval := NewStaticEvaluator(mod, call)
value, diags := eval.Evaluate(mod.Locals["ws"].Expr, dummyIdentifier)
if diags.HasErrors() {
t.Error(diags)
}
if value.AsString() != "my-workspace" {
t.Errorf("Expected %s got %s", "my-workspace", value.AsString())
}
})
t.Run("Functions", func(t *testing.T) {
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir")
eval := NewStaticEvaluator(mod, RootModuleCallForTesting())
value, diags := eval.Evaluate(mod.Locals["func"].Expr, dummyIdentifier)
if diags.HasErrors() {
t.Error(diags)
}
if value.AsString() != "f887f41a53a46e2d40a3f8f86cacaaa2" {
t.Errorf("Expected %s got %s", "f887f41a53a46e2d40a3f8f86cacaaa2", value.AsString())
}
_, diags = eval.Evaluate(mod.Locals["missing_func"].Expr, StaticIdentifier{Subject: fmt.Sprintf("local.%s", mod.Locals["missing_func"].Name), DeclRange: mod.Locals["missing_func"].DeclRange})
assertExactDiagnostics(t, diags, []string{`eval.tf:60,17-27: Call to unknown function; There is no function named "missing_fn".`})
_, diags = eval.Evaluate(mod.Locals["provider_func"].Expr, StaticIdentifier{Subject: fmt.Sprintf("local.%s", mod.Locals["provider_func"].Name), DeclRange: mod.Locals["provider_func"].DeclRange})
assertExactDiagnostics(t, diags, []string{`eval.tf:61,18-36: Provider function in static context; Unable to use provider::type::fn in static context, which is required by local.provider_func`})
})
}
func TestStaticEvaluator_DecodeExpression(t *testing.T) {
dummyIdentifier := StaticIdentifier{Subject: "local.test"}
parser := testParser(map[string]string{"eval.tf": ""})
file, fileDiags := parser.LoadConfigFile("eval.tf")
if fileDiags.HasErrors() {
t.Fatal(fileDiags)
}
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir")
eval := NewStaticEvaluator(mod, RootModuleCallForTesting())
cases := []struct {
expr string
diags []string
}{{
expr: `"static"`,
}, {
expr: `count`,
diags: []string{`eval.tf:1,1-6: Invalid reference; The "count" object cannot be accessed directly. Instead, access one of its attributes.`},
}, {
expr: `module.foo.bar`,
diags: []string{`eval.tf:1,1-15: Module output not supported in static context; Unable to use module.foo.bar in static context, which is required by local.test`},
}}
for _, tc := range cases {
t.Run(tc.expr, func(t *testing.T) {
expr, _ := hclsyntax.ParseExpression([]byte(tc.expr), "eval.tf", hcl.InitialPos)
var str string
diags := eval.DecodeExpression(expr, dummyIdentifier, &str)
assertExactDiagnostics(t, diags, tc.diags)
})
}
}
func TestStaticEvaluator_DecodeBlock(t *testing.T) {
cases := []struct {
ident string
body string
diags []string
}{{
ident: "valid",
body: `
locals {
static = "static"
}
terraform {
backend "valid" {
thing = local.static
}
}`,
}, {
ident: "badref",
body: `
terraform {
backend "badref" {
thing = count
}
}`,
diags: []string{`eval.tf:4,11-16: Invalid reference; The "count" object cannot be accessed directly. Instead, access one of its attributes.`},
}, {
ident: "badeval",
body: `
terraform {
backend "badeval" {
thing = module.foo.bar
}
}`,
diags: []string{`eval.tf:4,11-25: Module output not supported in static context; Unable to use module.foo.bar in static context, which is required by backend.badeval`},
}}
for _, tc := range cases {
t.Run(tc.ident, func(t *testing.T) {
parser := testParser(map[string]string{"eval.tf": tc.body})
file, fileDiags := parser.LoadConfigFile("eval.tf")
if fileDiags.HasErrors() {
t.Fatal(fileDiags)
}
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir")
_, diags := mod.Backend.Decode(&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"thing": &configschema.Attribute{
Type: cty.String,
},
},
})
assertExactDiagnostics(t, diags, tc.diags)
})
}
}

View File

@ -0,0 +1,267 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package configs
import (
"fmt"
"os"
"path/filepath"
"github.com/hashicorp/hcl/v2"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/didyoumean"
"github.com/opentofu/opentofu/internal/lang"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// newStaticScope creates a lang.Scope that's backed by the static view of the module represented by the StaticEvaluator
func newStaticScope(eval *StaticEvaluator, stack ...StaticIdentifier) *lang.Scope {
return &lang.Scope{
Data: staticScopeData{eval, stack},
ParseRef: addrs.ParseRef,
BaseDir: ".", // Always current working directory for now. (same as Evaluator.Scope())
PureOnly: false,
ConsoleMode: false,
}
}
// This structure represents the data required to evaluate a specific identifier reference (top of the stack)
// It is used by lang.Scope to link the given StaticEvaluator data to addrs.References in the current scope.
type staticScopeData struct {
eval *StaticEvaluator
stack []StaticIdentifier
}
// staticScopeData must implement lang.Data
var _ lang.Data = (*staticScopeData)(nil)
// Creates a nested scope to evaluate nested references
func (s staticScopeData) scope(ident StaticIdentifier) (*lang.Scope, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
for _, frame := range s.stack {
if frame.String() == ident.String() {
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Circular reference",
Detail: fmt.Sprintf("%s is self referential", ident.String()), // TODO use stack in error message
Subject: ident.DeclRange.Ptr(),
})
}
}
return newStaticScope(s.eval, append(s.stack, ident)...), diags
}
// If an error occurs when resolving a dependent value, we need to add additional context to the diagnostics
func (s staticScopeData) enhanceDiagnostics(ident StaticIdentifier, diags tfdiags.Diagnostics) tfdiags.Diagnostics {
if diags.HasErrors() {
top := s.stack[len(s.stack)-1]
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unable to compute static value",
Detail: fmt.Sprintf("%s depends on %s which is not available", top, ident.String()),
Subject: top.DeclRange.Ptr(),
})
}
return diags
}
// Early check to only allow references we expect in a static context
func (s staticScopeData) StaticValidateReferences(refs []*addrs.Reference, _ addrs.Referenceable, _ addrs.Referenceable) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
top := s.stack[len(s.stack)-1]
for _, ref := range refs {
switch subject := ref.Subject.(type) {
case addrs.LocalValue:
continue
case addrs.InputVariable:
continue
case addrs.PathAttr:
continue
case addrs.TerraformAttr:
continue
case addrs.ModuleCallInstanceOutput:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Module output not supported in static context",
Detail: fmt.Sprintf("Unable to use %s in static context, which is required by %s", subject.String(), top.String()),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
case addrs.ProviderFunction:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Provider function in static context",
Detail: fmt.Sprintf("Unable to use %s in static context, which is required by %s", subject.String(), top.String()),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Dynamic value in static context",
Detail: fmt.Sprintf("Unable to use %s in static context, which is required by %s", subject.String(), top.String()),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
}
}
return diags
}
func (s staticScopeData) GetCountAttr(addrs.CountAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
panic("Not Available in Static Context")
}
func (s staticScopeData) GetForEachAttr(addrs.ForEachAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
panic("Not Available in Static Context")
}
func (s staticScopeData) GetResource(addrs.Resource, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
panic("Not Available in Static Context")
}
func (s staticScopeData) GetLocalValue(ident addrs.LocalValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
local, ok := s.eval.cfg.Locals[ident.Name]
if !ok {
return cty.DynamicVal, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Undefined local",
Detail: fmt.Sprintf("Undefined local %s", ident.String()),
Subject: rng.ToHCL().Ptr(),
})
}
id := StaticIdentifier{
Module: s.eval.call.addr,
Subject: fmt.Sprintf("local.%s", local.Name),
DeclRange: local.DeclRange,
}
scope, scopeDiags := s.scope(id)
diags = diags.Append(scopeDiags)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
val, valDiags := scope.EvalExpr(local.Expr, cty.DynamicPseudoType)
return val, s.enhanceDiagnostics(id, diags.Append(valDiags))
}
func (s staticScopeData) GetModule(addrs.ModuleCall, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
panic("Not Available in Static Context")
}
func (s staticScopeData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// TODO this is copied and trimed down from tofu/evaluate.go GetPathAttr. Ideally this should be refactored to a common location.
var diags tfdiags.Diagnostics
switch addr.Name {
case "cwd":
wd, err := os.Getwd()
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Failed to get working directory`,
Detail: fmt.Sprintf(`The value for path.cwd cannot be determined due to a system error: %s`, err),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
wd, err = filepath.Abs(wd)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Failed to get working directory`,
Detail: fmt.Sprintf(`The value for path.cwd cannot be determined due to a system error: %s`, err),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
return cty.StringVal(filepath.ToSlash(wd)), diags
case "module":
return cty.StringVal(s.eval.cfg.SourceDir), diags
case "root":
return cty.StringVal(s.eval.call.rootPath), diags
default:
suggestion := didyoumean.NameSuggestion(addr.Name, []string{"cwd", "module", "root"})
if suggestion != "" {
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid "path" attribute`,
Detail: fmt.Sprintf(`The "path" object does not have an attribute named %q.%s`, addr.Name, suggestion),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
}
func (s staticScopeData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// TODO this is copied and trimed down from tofu/evaluate.go GetTerraformAttr. Ideally this should be refactored to a common location.
var diags tfdiags.Diagnostics
switch addr.Name {
case "workspace":
workspaceName := s.eval.call.workspace
return cty.StringVal(workspaceName), diags
case "env":
// Prior to Terraform 0.12 there was an attribute "env", which was
// an alias name for "workspace". This was deprecated and is now
// removed.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid "terraform" attribute`,
Detail: `The terraform.env attribute was deprecated in v0.10 and removed in v0.12. The "state environment" concept was renamed to "workspace" in v0.12, and so the workspace name can now be accessed using the terraform.workspace attribute.`,
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid "terraform" attribute`,
Detail: fmt.Sprintf(`The "terraform" object does not have an attribute named %q. The only supported attribute is terraform.workspace, the name of the currently-selected workspace.`, addr.Name),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
}
func (s staticScopeData) GetInputVariable(ident addrs.InputVariable, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
variable, ok := s.eval.cfg.Variables[ident.Name]
if !ok {
return cty.NilVal, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Undefined variable",
Detail: fmt.Sprintf("Undefined variable %s", ident.String()),
Subject: rng.ToHCL().Ptr(),
})
}
id := StaticIdentifier{
Module: s.eval.call.addr,
Subject: fmt.Sprintf("var.%s", variable.Name),
DeclRange: variable.DeclRange,
}
val, valDiags := s.eval.call.vars(variable)
return val, s.enhanceDiagnostics(id, diags.Append(valDiags))
}
func (s staticScopeData) GetOutput(addrs.OutputValue, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
panic("Not Available in Static Context")
}
func (s staticScopeData) GetCheckBlock(addrs.Check, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
panic("Not Available in Static Context")
}

View File

@ -1,6 +1,6 @@
variable "module_version" { default = "v1.0" }
module "foo" {
source = "./ff"
source = "mod/foo/foo"
version = var.module_version
}

View File

@ -24,14 +24,19 @@ type VersionConstraint struct {
}
func decodeVersionConstraint(attr *hcl.Attribute) (VersionConstraint, hcl.Diagnostics) {
val, diags := attr.Expr.Value(nil)
if diags.HasErrors() {
return VersionConstraint{}, diags
}
return decodeVersionConstraintValue(attr, val)
}
func decodeVersionConstraintValue(attr *hcl.Attribute, val cty.Value) (VersionConstraint, hcl.Diagnostics) {
var diags hcl.Diagnostics
ret := VersionConstraint{
DeclRange: attr.Range,
}
val, diags := attr.Expr.Value(nil)
if diags.HasErrors() {
return ret, diags
}
var err error
val, err = convert.Convert(val, cty.String)
if err != nil {

View File

@ -20,6 +20,7 @@ import (
"github.com/opentofu/opentofu/internal/configs/configload"
"github.com/opentofu/opentofu/internal/copy"
"github.com/opentofu/opentofu/internal/getmodules"
"github.com/zclconf/go-cty/cty"
version "github.com/hashicorp/go-version"
"github.com/opentofu/opentofu/internal/modsdir"
@ -138,16 +139,18 @@ func DirFromModule(ctx context.Context, loader *configload.Loader, rootDir, modu
fmt.Sprintf("Failed to parse module source address: %s", err),
))
}
rng := hcl.Range{
Filename: initFromModuleRootFilename,
Start: hcl.InitialPos,
End: hcl.InitialPos,
}
fakeRootModule := &configs.Module{
ModuleCalls: map[string]*configs.ModuleCall{
initFromModuleRootCallName: {
Name: initFromModuleRootCallName,
SourceAddr: sourceAddr,
DeclRange: hcl.Range{
Filename: initFromModuleRootFilename,
Start: hcl.InitialPos,
End: hcl.InitialPos,
},
Source: hcl.StaticExpr(cty.StringVal(sourceAddrStr), rng),
DeclRange: rng,
},
},
ProviderRequirements: &configs.RequiredProviders{},
@ -213,7 +216,7 @@ func DirFromModule(ctx context.Context, loader *configload.Loader, rootDir, modu
// and must thus be rewritten to be absolute addresses again.
// For now we can't do this rewriting automatically, but we'll
// generate an error to help the user do it manually.
mod, _ := loader.Parser().LoadConfigDir(rootDir) // ignore diagnostics since we're just doing value-add here anyway
mod, _ := loader.Parser().LoadConfigDir(rootDir, configs.NewStaticModuleCall(addrs.RootModule, nil, rootDir, "")) // ignore diagnostics since we're just doing value-add here anyway
if mod != nil {
for _, mc := range mod.ModuleCalls {
if pathTraversesUp(mc.SourceAddrRaw) {

View File

@ -110,7 +110,7 @@ func TestDirFromModule_registry(t *testing.T) {
// Make sure the configuration is loadable now.
// (This ensures that correct information is recorded in the manifest.)
config, loadDiags := loader.LoadConfig(".")
config, loadDiags := loader.LoadConfig(".", configs.RootModuleCallForTesting())
if assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) {
return
}
@ -192,7 +192,7 @@ func TestDirFromModule_submodules(t *testing.T) {
// Make sure the configuration is loadable now.
// (This ensures that correct information is recorded in the manifest.)
config, loadDiags := loader.LoadConfig(".")
config, loadDiags := loader.LoadConfig(".", configs.RootModuleCallForTesting())
if assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) {
return
}
@ -323,7 +323,7 @@ func TestDirFromModule_rel_submodules(t *testing.T) {
// Make sure the configuration is loadable now.
// (This ensures that correct information is recorded in the manifest.)
config, loadDiags := loader.LoadConfig(".")
config, loadDiags := loader.LoadConfig(".", configs.RootModuleCallForTesting())
if assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) {
return
}

View File

@ -94,11 +94,11 @@ func NewModuleInstaller(modsDir string, loader *configload.Loader, reg *registry
// If successful (the returned diagnostics contains no errors) then the
// first return value is the early configuration tree that was constructed by
// the installation process.
func (i *ModuleInstaller) InstallModules(ctx context.Context, rootDir, testsDir string, upgrade, installErrsOnly bool, hooks ModuleInstallHooks) (*configs.Config, tfdiags.Diagnostics) {
func (i *ModuleInstaller) InstallModules(ctx context.Context, rootDir, testsDir string, upgrade, installErrsOnly bool, hooks ModuleInstallHooks, call configs.StaticModuleCall) (*configs.Config, tfdiags.Diagnostics) {
log.Printf("[TRACE] ModuleInstaller: installing child modules for %s into %s", rootDir, i.modsDir)
var diags tfdiags.Diagnostics
rootMod, mDiags := i.loader.Parser().LoadConfigDirWithTests(rootDir, testsDir)
rootMod, mDiags := i.loader.Parser().LoadConfigDirWithTests(rootDir, testsDir, call)
if rootMod == nil {
// We drop the diagnostics here because we only want to report module
// loading errors after checking the core version constraints, which we
@ -240,7 +240,7 @@ func (i *ModuleInstaller) moduleInstallWalker(ctx context.Context, manifest mods
// keep our existing record.
info, err := os.Stat(record.Dir)
if err == nil && info.IsDir() {
mod, mDiags := i.loader.Parser().LoadConfigDir(record.Dir)
mod, mDiags := i.loader.Parser().LoadConfigDir(record.Dir, req.Call)
if mod == nil {
// nil indicates an unreadable module, which should never happen,
// so we return the full loader diagnostics here.
@ -381,7 +381,7 @@ func (i *ModuleInstaller) installLocalModule(req *configs.ModuleRequest, key str
}
// Finally we are ready to try actually loading the module.
mod, mDiags := i.loader.Parser().LoadConfigDir(newDir)
mod, mDiags := i.loader.Parser().LoadConfigDir(newDir, req.Call)
if mod == nil {
// nil indicates missing or unreadable directory, so we'll
// discard the returned diags and return a more specific
@ -663,7 +663,7 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *config
log.Printf("[TRACE] ModuleInstaller: %s should now be at %s", key, modDir)
// Finally we are ready to try actually loading the module.
mod, mDiags := i.loader.Parser().LoadConfigDir(modDir)
mod, mDiags := i.loader.Parser().LoadConfigDir(modDir, req.Call)
if mod == nil {
// nil indicates missing or unreadable directory, so we'll
// discard the returned diags and return a more specific
@ -764,7 +764,7 @@ func (i *ModuleInstaller) installGoGetterModule(ctx context.Context, req *config
log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, addr, modDir)
// Finally we are ready to try actually loading the module.
mod, mDiags := i.loader.Parser().LoadConfigDir(modDir)
mod, mDiags := i.loader.Parser().LoadConfigDir(modDir, req.Call)
if mod == nil {
// nil indicates missing or unreadable directory, so we'll
// discard the returned diags and return a more specific

View File

@ -47,7 +47,7 @@ func TestModuleInstaller(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
wantCalls := []testInstallHookCall{
@ -78,7 +78,7 @@ func TestModuleInstaller(t *testing.T) {
// Make sure the configuration is loadable now.
// (This ensures that correct information is recorded in the manifest.)
config, loadDiags := loader.LoadConfig(".")
config, loadDiags := loader.LoadConfig(".", configs.RootModuleCallForTesting())
assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags))
wantTraces := map[string]string{
@ -111,7 +111,7 @@ func TestModuleInstaller_error(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
if !diags.HasErrors() {
t.Fatal("expected error")
@ -132,7 +132,7 @@ func TestModuleInstaller_emptyModuleName(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
if !diags.HasErrors() {
t.Fatal("expected error")
@ -153,7 +153,7 @@ func TestModuleInstaller_invalidModuleName(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks)
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks, configs.RootModuleCallForTesting())
if !diags.HasErrors() {
t.Fatal("expected error")
} else {
@ -190,7 +190,7 @@ func TestModuleInstaller_packageEscapeError(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
if !diags.HasErrors() {
t.Fatal("expected error")
@ -228,7 +228,7 @@ func TestModuleInstaller_explicitPackageBoundary(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
if diags.HasErrors() {
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
@ -251,7 +251,7 @@ func TestModuleInstaller_ExactMatchPrerelease(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
if diags.HasErrors() {
t.Fatalf("found unexpected errors: %s", diags.Err())
@ -278,7 +278,7 @@ func TestModuleInstaller_PartialMatchPrerelease(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
if diags.HasErrors() {
t.Fatalf("found unexpected errors: %s", diags.Err())
@ -301,7 +301,7 @@ func TestModuleInstaller_invalid_version_constraint_error(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
if !diags.HasErrors() {
t.Fatal("expected error")
@ -327,7 +327,7 @@ func TestModuleInstaller_invalidVersionConstraintGetter(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
if !diags.HasErrors() {
t.Fatal("expected error")
@ -353,7 +353,7 @@ func TestModuleInstaller_invalidVersionConstraintLocal(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
if !diags.HasErrors() {
t.Fatal("expected error")
@ -379,7 +379,7 @@ func TestModuleInstaller_symlink(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
wantCalls := []testInstallHookCall{
@ -410,7 +410,7 @@ func TestModuleInstaller_symlink(t *testing.T) {
// Make sure the configuration is loadable now.
// (This ensures that correct information is recorded in the manifest.)
config, loadDiags := loader.LoadConfig(".")
config, loadDiags := loader.LoadConfig(".", configs.RootModuleCallForTesting())
assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags))
wantTraces := map[string]string{
@ -455,7 +455,7 @@ func TestLoaderInstallModules_registry(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks)
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks, configs.RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
v := version.Must(version.NewVersion("0.0.1"))
@ -569,7 +569,7 @@ func TestLoaderInstallModules_registry(t *testing.T) {
// Make sure the configuration is loadable now.
// (This ensures that correct information is recorded in the manifest.)
config, loadDiags := loader.LoadConfig(".")
config, loadDiags := loader.LoadConfig(".", configs.RootModuleCallForTesting())
assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags))
wantTraces := map[string]string{
@ -618,7 +618,7 @@ func TestLoaderInstallModules_goGetter(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks)
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks, configs.RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
wantCalls := []testInstallHookCall{
@ -699,7 +699,7 @@ func TestLoaderInstallModules_goGetter(t *testing.T) {
// Make sure the configuration is loadable now.
// (This ensures that correct information is recorded in the manifest.)
config, loadDiags := loader.LoadConfig(".")
config, loadDiags := loader.LoadConfig(".", configs.RootModuleCallForTesting())
assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags))
wantTraces := map[string]string{
@ -736,7 +736,7 @@ func TestModuleInstaller_fromTests(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
wantCalls := []testInstallHookCall{
@ -761,7 +761,7 @@ func TestModuleInstaller_fromTests(t *testing.T) {
// Make sure the configuration is loadable now.
// (This ensures that correct information is recorded in the manifest.)
config, loadDiags := loader.LoadConfigWithTests(".", "tests")
config, loadDiags := loader.LoadConfigWithTests(".", "tests", configs.RootModuleCallForTesting())
assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags))
if config.Module.Tests[filepath.Join("tests", "main.tftest.hcl")].Runs[0].ConfigUnderTest == nil {
@ -793,7 +793,7 @@ func TestLoadInstallModules_registryFromTest(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks)
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks, configs.RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
v := version.Must(version.NewVersion("0.0.1"))
@ -870,7 +870,7 @@ func TestLoadInstallModules_registryFromTest(t *testing.T) {
// Make sure the configuration is loadable now.
// (This ensures that correct information is recorded in the manifest.)
config, loadDiags := loader.LoadConfigWithTests(".", "tests")
config, loadDiags := loader.LoadConfigWithTests(".", "tests", configs.RootModuleCallForTesting())
assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags))
if config.Module.Tests["main.tftest.hcl"].Runs[0].ConfigUnderTest == nil {

View File

@ -41,7 +41,8 @@ func LoadConfigForTests(t *testing.T, rootDir string, testsDir string) (*configs
loader, cleanup := configload.NewLoaderForTests(t)
inst := NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
_, moreDiags := inst.InstallModules(context.Background(), rootDir, testsDir, true, false, ModuleInstallHooksImpl{})
call := configs.RootModuleCallForTesting()
_, moreDiags := inst.InstallModules(context.Background(), rootDir, testsDir, true, false, ModuleInstallHooksImpl{}, call)
diags = diags.Append(moreDiags)
if diags.HasErrors() {
cleanup()
@ -55,7 +56,7 @@ func LoadConfigForTests(t *testing.T, rootDir string, testsDir string) (*configs
t.Fatalf("failed to refresh modules after installation: %s", err)
}
config, hclDiags := loader.LoadConfig(rootDir)
config, hclDiags := loader.LoadConfig(rootDir, call)
diags = diags.Append(hclDiags)
return config, loader, cleanup, diags
}

View File

@ -13,6 +13,7 @@ import (
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configload"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/initwd"
@ -27,7 +28,7 @@ func testAnalyzer(t *testing.T, fixtureName string) *Analyzer {
defer cleanup()
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
_, instDiags := inst.InstallModules(context.Background(), configDir, "tests", true, false, initwd.ModuleInstallHooksImpl{})
_, instDiags := inst.InstallModules(context.Background(), configDir, "tests", true, false, initwd.ModuleInstallHooksImpl{}, configs.RootModuleCallForTesting())
if instDiags.HasErrors() {
t.Fatalf("unexpected module installation errors: %s", instDiags.Err().Error())
}
@ -35,7 +36,7 @@ func testAnalyzer(t *testing.T, fixtureName string) *Analyzer {
t.Fatalf("failed to refresh modules after install: %s", err)
}
cfg, loadDiags := loader.LoadConfig(configDir)
cfg, loadDiags := loader.LoadConfig(configDir, configs.RootModuleCallForTesting())
if loadDiags.HasErrors() {
t.Fatalf("unexpected configuration errors: %s", loadDiags.Error())
}

View File

@ -14,6 +14,7 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configload"
)
@ -26,7 +27,7 @@ func TestConfigSnapshotRoundtrip(t *testing.T) {
t.Fatal(err)
}
_, snapIn, diags := loader.LoadConfigWithSnapshot(fixtureDir)
_, snapIn, diags := loader.LoadConfigWithSnapshot(fixtureDir, configs.RootModuleCallForTesting())
if diags.HasErrors() {
t.Fatal(diags.Error())
}

View File

@ -13,6 +13,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configload"
"github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/encryption"
@ -32,7 +33,7 @@ func TestRoundtrip(t *testing.T) {
t.Fatal(err)
}
_, snapIn, diags := loader.LoadConfigWithSnapshot(fixtureDir)
_, snapIn, diags := loader.LoadConfigWithSnapshot(fixtureDir, configs.RootModuleCallForTesting())
if diags.HasErrors() {
t.Fatal(diags.Error())
}
@ -160,7 +161,7 @@ func TestRoundtrip(t *testing.T) {
// Reading from snapshots is tested in the configload package, so
// here we'll just test that we can successfully do it, to see if the
// glue code in _this_ package is correct.
_, diags := pr.ReadConfig()
_, diags := pr.ReadConfig(configs.RootModuleCallForTesting())
if diags.HasErrors() {
t.Errorf("when reading config: %s", diags.Err())
}

View File

@ -213,7 +213,7 @@ func (r *Reader) ReadConfigSnapshot() (*configload.Snapshot, error) {
// Internally this function delegates to the configs/configload package to
// parse the embedded configuration and so it returns diagnostics (rather than
// a native Go error as with other methods on Reader).
func (r *Reader) ReadConfig() (*configs.Config, tfdiags.Diagnostics) {
func (r *Reader) ReadConfig(rootCall configs.StaticModuleCall) (*configs.Config, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
snap, err := r.ReadConfigSnapshot()
@ -228,7 +228,7 @@ func (r *Reader) ReadConfig() (*configs.Config, tfdiags.Diagnostics) {
loader := configload.NewLoaderFromSnapshot(snap)
rootDir := snap.Modules[""].Dir // Root module base directory
config, configDiags := loader.LoadConfig(rootDir)
config, configDiags := loader.LoadConfig(rootDir, rootCall)
diags = diags.Append(configDiags)
return config, diags

View File

@ -540,7 +540,7 @@ func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instance
defer cleanup()
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{})
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{}, configs.RootModuleCallForTesting())
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())
}
@ -551,7 +551,7 @@ func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instance
t.Fatalf("failed to refresh modules after installation: %s", err)
}
rootCfg, diags := loader.LoadConfig(dir)
rootCfg, diags := loader.LoadConfig(dir, configs.RootModuleCallForTesting())
if diags.HasErrors() {
t.Fatalf("failed to load root module: %s", diags.Error())
}

View File

@ -742,7 +742,7 @@ func contextOptsForPlanViaFile(t *testing.T, configSnap *configload.Snapshot, pl
return nil, nil, nil, err
}
config, diags := pr.ReadConfig()
config, diags := pr.ReadConfig(configs.RootModuleCallForTesting())
if diags.HasErrors() {
return nil, nil, nil, diags.Err()
}

View File

@ -69,7 +69,7 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config
// sources only this ultimately just records all of the module paths
// in a JSON file so that we can load them below.
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{})
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{}, configs.RootModuleCallForTesting())
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())
}
@ -80,7 +80,7 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config
t.Fatalf("failed to refresh modules after installation: %s", err)
}
config, snap, diags := loader.LoadConfigWithSnapshot(dir)
config, snap, diags := loader.LoadConfigWithSnapshot(dir, configs.RootModuleCallForTesting())
if diags.HasErrors() {
t.Fatal(diags.Error())
}
@ -126,7 +126,7 @@ func testModuleInline(t *testing.T, sources map[string]string) *configs.Config {
// sources only this ultimately just records all of the module paths
// in a JSON file so that we can load them below.
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
_, instDiags := inst.InstallModules(context.Background(), cfgPath, "tests", true, false, initwd.ModuleInstallHooksImpl{})
_, instDiags := inst.InstallModules(context.Background(), cfgPath, "tests", true, false, initwd.ModuleInstallHooksImpl{}, configs.RootModuleCallForTesting())
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())
}
@ -137,7 +137,7 @@ func testModuleInline(t *testing.T, sources map[string]string) *configs.Config {
t.Fatalf("failed to refresh modules after installation: %s", err)
}
config, diags := loader.LoadConfigWithTests(cfgPath, "tests")
config, diags := loader.LoadConfigWithTests(cfgPath, "tests", configs.RootModuleCallForTesting())
if diags.HasErrors() {
t.Fatal(diags.Error())
}

View File

@ -219,7 +219,7 @@ func TestMigrateStateProviderAddresses(t *testing.T) {
var cfg *configs.Config
if tt.args.configDir != "" {
var hclDiags hcl.Diagnostics
cfg, hclDiags = loader.LoadConfig(tt.args.configDir)
cfg, hclDiags = loader.LoadConfig(tt.args.configDir, configs.RootModuleCallForTesting())
if hclDiags.HasErrors() {
t.Fatalf("invalid configuration: %s", hclDiags.Error())
}

View File

@ -463,3 +463,28 @@ OpenTofu will still extract the entire package to local disk, but will read
the module from the subdirectory. As a result, it is safe for a module in
a sub-directory of a package to use [a local path](#local-paths) to another
module as long as it is in the _same_ package.
## Support for Variable and Local Interpolation
As projects grow in complexity and requirements, it is prudent to consider using locals and variables in the module source and version fields.
Many organizations utilize the mono-repo pattern for modules:
```hcl
locals {
modules_repo = "github.com/myorg/tofu-modules/"
modules_version = "?ref=v1.20.4"
}
module "storage" {
source = "${local.modules_repo}/storage${local.modules_version}"
}
module "compute" {
source = "${local.modules_repo}/compute${local.modules_version}"
}
```
It is quite easy to then update the version for a patch release, or to switch to a fork of the repository.
:::note
The source and version fields may not contain any references to data in the state or provider defined functions. All value must be able to be resolved during `tofu init` before the state is available.
:::

View File

@ -62,8 +62,7 @@ Module calls use the following kinds of arguments:
All modules **require** a `source` argument, which is a meta-argument defined by
OpenTofu. Its value is either the path to a local directory containing the
module's configuration files, or a remote module source that OpenTofu should
download and use. This value must be a literal string with no template
sequences; arbitrary expressions are not allowed. For more information on
download and use. For more information on
possible values for this argument, see [Module Sources](../../language/modules/sources.mdx).
The same source address can be specified in multiple `module` blocks to create

View File

@ -41,7 +41,7 @@ terraform {
There are some important limitations on backend configuration:
- A configuration can only provide one backend block.
- A backend block cannot refer to named values (like input variables, locals, or data source attributes).
- A backend block cannot refer to values in the state or locals derived from the state (data source attributes).
### Credentials and Sensitive Data