mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-23 07:33:32 -06:00
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:
parent
ab289fc07c
commit
8f8e0aa4aa
@ -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))
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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])
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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()) }
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
|
7
internal/command/testdata/init-module-variable-source/main.tf
vendored
Normal file
7
internal/command/testdata/init-module-variable-source/main.tf
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
variable "src" {
|
||||
type = string
|
||||
}
|
||||
|
||||
module "mod" {
|
||||
source = var.src
|
||||
}
|
0
internal/command/testdata/init-module-variable-source/mod/mod.tf
vendored
Normal file
0
internal/command/testdata/init-module-variable-source/mod/mod.tf
vendored
Normal file
10
internal/command/testdata/init-module-variable-version/main.tf
vendored
Normal file
10
internal/command/testdata/init-module-variable-version/main.tf
vendored
Normal 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)
|
||||
}
|
@ -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": {
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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")
|
||||
|
@ -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()))
|
||||
|
@ -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")
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)...)
|
||||
|
@ -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())
|
||||
}
|
||||
|
119
internal/configs/static_evaluator.go
Normal file
119
internal/configs/static_evaluator.go
Normal 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
|
||||
}
|
397
internal/configs/static_evaluator_test.go
Normal file
397
internal/configs/static_evaluator_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
267
internal/configs/static_scope.go
Normal file
267
internal/configs/static_scope.go
Normal 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")
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
variable "module_version" { default = "v1.0" }
|
||||
|
||||
module "foo" {
|
||||
source = "./ff"
|
||||
source = "mod/foo/foo"
|
||||
version = var.module_version
|
||||
}
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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.
|
||||
:::
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user