command: "console" now accepts a -scope argument

The -scope argument specifies a scope other than the root module to
evaluate expressions in. Currently we only support module instance
addresses as scope addresses but this is generalized to allow potentially
supporting other evaluation scopes in future if we find reasons to support
that, such as evaluating in the scope of a particular resource instance
to find out what its each.value is.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Martin Atkins 2024-12-17 14:49:03 -08:00
parent 3de0333560
commit b7038ef037
2 changed files with 68 additions and 5 deletions

View File

@ -29,10 +29,12 @@ type ConsoleCommand struct {
func (c *ConsoleCommand) Run(args []string) int {
ctx := c.CommandContext()
var scopeAddrStr string
args = c.Meta.process(args)
cmdFlags := c.Meta.extendedFlagSet("console")
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&scopeAddrStr, "scope", "", "evaluation scope address")
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()))
@ -54,6 +56,18 @@ func (c *ConsoleCommand) Run(args []string) int {
var diags tfdiags.Diagnostics
scopeAddr := addrs.ExprScope(addrs.RootModuleInstance)
if scopeAddrStr != "" {
// User is trying to specify a scope other than the root module.
var scopeAddrDiags tfdiags.Diagnostics
scopeAddr, scopeAddrDiags = addrs.ParseExprScopeStr(scopeAddrStr)
diags = diags.Append(scopeAddrDiags)
if scopeAddrDiags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
}
// Load the encryption configuration
enc, encDiags := c.EncryptionFromPath(configPath)
diags = diags.Append(encDiags)
@ -146,7 +160,7 @@ func (c *ConsoleCommand) Run(args []string) int {
// Before we can evaluate expressions, we must compute and populate any
// derived values (input variables, local values, output values)
// that are not stored in the persistent state.
scope, scopeDiags := lr.Core.Eval(ctx, lr.Config, lr.InputState, addrs.RootModuleInstance, evalOpts)
scope, scopeDiags := lr.Core.Eval(ctx, lr.Config, lr.InputState, scopeAddr, evalOpts)
diags = diags.Append(scopeDiags)
if scope == nil {
// scope is nil if there are errors so bad that we can't even build a scope.
@ -214,12 +228,12 @@ func (c *ConsoleCommand) Help() string {
Usage: tofu [global options] console [options]
Starts an interactive console for experimenting with OpenTofu
interpolations.
expressions.
This will open an interactive console that you can use to type
interpolations into and inspect their values. This command loads the
current state. This lets you explore and test interpolations before
using them in future configurations.
expressions into and inspect their values. This command loads the
current state. This lets you explore and test expressions before
using them in your modules.
This command will never modify your state.
@ -237,6 +251,11 @@ Options:
will be performed. All locations, for all errors
will be listed. Disabled by default
-scope=ADDR Choose the scope where expressions will be evaluated.
Currently this must be a module instance address.
If unspecified, expressions are evaluated in the root
module's scope.
-state=path Legacy option for the local backend only. See the local
backend's documentation for more information.

View File

@ -243,6 +243,50 @@ func TestConsole_modules(t *testing.T) {
}
}
func TestConsole_modulesNestedScope(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("modules"), td)
defer testChdir(t, td)()
p := applyFixtureProvider()
ui := cli.NewMockUi()
view, _ := testView(t)
c := &ConsoleCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
View: view,
},
}
args := []string{"-scope=module.count_child[0]"}
commands := map[string]string{
// Since the resource instance hasn't been applied yet it
// returns an unknown value, but the important thing here
// is that it doesn't fail because test_instance.test is
// declared inside module.count_child[0] and is
// therefore valid to refer to in that scope.
"test_instance.test\n": "(known after apply)\n",
}
for cmd, val := range commands {
var output bytes.Buffer
defer testStdinPipe(t, strings.NewReader(cmd))()
outCloser := testStdoutCapture(t, &output)
code := c.Run(args)
outCloser()
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := output.String()
if output.String() != val {
t.Fatalf("bad: %q, expected %q", actual, val)
}
}
}
func TestConsole_multiline_pipe(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("console-multiline-vars"), td)