github.com/opentofu/opentofu@v1.7.1/internal/command/console.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package command
     7  
     8  import (
     9  	"bufio"
    10  	"fmt"
    11  	"os"
    12  	"strings"
    13  
    14  	"github.com/opentofu/opentofu/internal/addrs"
    15  	"github.com/opentofu/opentofu/internal/backend"
    16  	"github.com/opentofu/opentofu/internal/command/arguments"
    17  	"github.com/opentofu/opentofu/internal/repl"
    18  	"github.com/opentofu/opentofu/internal/tfdiags"
    19  	"github.com/opentofu/opentofu/internal/tofu"
    20  
    21  	"github.com/mitchellh/cli"
    22  )
    23  
    24  // ConsoleCommand is a Command implementation that starts an interactive
    25  // console that can be used to try expressions with the current config.
    26  type ConsoleCommand struct {
    27  	Meta
    28  }
    29  
    30  func (c *ConsoleCommand) Run(args []string) int {
    31  	args = c.Meta.process(args)
    32  	cmdFlags := c.Meta.extendedFlagSet("console")
    33  	cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
    34  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    35  	if err := cmdFlags.Parse(args); err != nil {
    36  		c.Ui.Error(fmt.Sprintf("Error parsing command line flags: %s\n", err.Error()))
    37  		return 1
    38  	}
    39  
    40  	configPath, err := modulePath(cmdFlags.Args())
    41  	if err != nil {
    42  		c.Ui.Error(err.Error())
    43  		return 1
    44  	}
    45  	configPath = c.Meta.normalizePath(configPath)
    46  
    47  	// Check for user-supplied plugin path
    48  	if c.pluginPath, err = c.loadPluginPath(); err != nil {
    49  		c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err))
    50  		return 1
    51  	}
    52  
    53  	var diags tfdiags.Diagnostics
    54  
    55  	// Load the encryption configuration
    56  	enc, encDiags := c.EncryptionFromPath(configPath)
    57  	diags = diags.Append(encDiags)
    58  	if encDiags.HasErrors() {
    59  		c.showDiagnostics(diags)
    60  		return 1
    61  	}
    62  
    63  	backendConfig, backendDiags := c.loadBackendConfig(configPath)
    64  	diags = diags.Append(backendDiags)
    65  	if diags.HasErrors() {
    66  		c.showDiagnostics(diags)
    67  		return 1
    68  	}
    69  
    70  	// Load the backend
    71  	b, backendDiags := c.Backend(&BackendOpts{
    72  		Config: backendConfig,
    73  	}, enc.State())
    74  	diags = diags.Append(backendDiags)
    75  	if backendDiags.HasErrors() {
    76  		c.showDiagnostics(diags)
    77  		return 1
    78  	}
    79  
    80  	// We require a local backend
    81  	local, ok := b.(backend.Local)
    82  	if !ok {
    83  		c.showDiagnostics(diags) // in case of any warnings in here
    84  		c.Ui.Error(ErrUnsupportedLocalOp)
    85  		return 1
    86  	}
    87  
    88  	// This is a read-only command
    89  	c.ignoreRemoteVersionConflict(b)
    90  
    91  	// Build the operation
    92  	opReq := c.Operation(b, arguments.ViewHuman, enc)
    93  	opReq.ConfigDir = configPath
    94  	opReq.ConfigLoader, err = c.initConfigLoader()
    95  	opReq.AllowUnsetVariables = true // we'll just evaluate them as unknown
    96  	if err != nil {
    97  		diags = diags.Append(err)
    98  		c.showDiagnostics(diags)
    99  		return 1
   100  	}
   101  
   102  	{
   103  		var moreDiags tfdiags.Diagnostics
   104  		opReq.Variables, moreDiags = c.collectVariableValues()
   105  		diags = diags.Append(moreDiags)
   106  		if moreDiags.HasErrors() {
   107  			c.showDiagnostics(diags)
   108  			return 1
   109  		}
   110  	}
   111  
   112  	// Get the context
   113  	lr, _, ctxDiags := local.LocalRun(opReq)
   114  	diags = diags.Append(ctxDiags)
   115  	if ctxDiags.HasErrors() {
   116  		c.showDiagnostics(diags)
   117  		return 1
   118  	}
   119  
   120  	// Successfully creating the context can result in a lock, so ensure we release it
   121  	defer func() {
   122  		diags := opReq.StateLocker.Unlock()
   123  		if diags.HasErrors() {
   124  			c.showDiagnostics(diags)
   125  		}
   126  	}()
   127  
   128  	// Set up the UI so we can output directly to stdout
   129  	ui := &cli.BasicUi{
   130  		Writer:      os.Stdout,
   131  		ErrorWriter: os.Stderr,
   132  	}
   133  
   134  	evalOpts := &tofu.EvalOpts{}
   135  	if lr.PlanOpts != nil {
   136  		// the LocalRun type is built primarily to support the main operations,
   137  		// so the variable values end up in the "PlanOpts" even though we're
   138  		// not actually making a plan.
   139  		evalOpts.SetVariables = lr.PlanOpts.SetVariables
   140  	}
   141  
   142  	// Before we can evaluate expressions, we must compute and populate any
   143  	// derived values (input variables, local values, output values)
   144  	// that are not stored in the persistent state.
   145  	scope, scopeDiags := lr.Core.Eval(lr.Config, lr.InputState, addrs.RootModuleInstance, evalOpts)
   146  	diags = diags.Append(scopeDiags)
   147  	if scope == nil {
   148  		// scope is nil if there are errors so bad that we can't even build a scope.
   149  		// Otherwise, we'll try to eval anyway.
   150  		c.showDiagnostics(diags)
   151  		return 1
   152  	}
   153  
   154  	// set the ConsoleMode to true so any available console-only functions included.
   155  	scope.ConsoleMode = true
   156  
   157  	if diags.HasErrors() {
   158  		diags = diags.Append(tfdiags.SimpleWarning("Due to the problems above, some expressions may produce unexpected results."))
   159  	}
   160  
   161  	// Before we become interactive we'll show any diagnostics we encountered
   162  	// during initialization, and then afterwards the driver will manage any
   163  	// further diagnostics itself.
   164  	c.showDiagnostics(diags)
   165  
   166  	// IO Loop
   167  	session := &repl.Session{
   168  		Scope: scope,
   169  	}
   170  
   171  	// Determine if stdin is a pipe. If so, we evaluate directly.
   172  	if c.StdinPiped() {
   173  		return c.modePiped(session, ui)
   174  	}
   175  
   176  	return c.modeInteractive(session, ui)
   177  }
   178  
   179  func (c *ConsoleCommand) modePiped(session *repl.Session, ui cli.Ui) int {
   180  	var lastResult string
   181  	scanner := bufio.NewScanner(os.Stdin)
   182  	for scanner.Scan() {
   183  		result, exit, diags := session.Handle(strings.TrimSpace(scanner.Text()))
   184  		if diags.HasErrors() {
   185  			// In piped mode we'll exit immediately on error.
   186  			c.showDiagnostics(diags)
   187  			return 1
   188  		}
   189  		if exit {
   190  			return 0
   191  		}
   192  
   193  		// Store the last result
   194  		lastResult = result
   195  	}
   196  
   197  	// Output the final result
   198  	ui.Output(lastResult)
   199  
   200  	return 0
   201  }
   202  
   203  func (c *ConsoleCommand) Help() string {
   204  	helpText := `
   205  Usage: tofu [global options] console [options]
   206  
   207    Starts an interactive console for experimenting with OpenTofu
   208    interpolations.
   209  
   210    This will open an interactive console that you can use to type
   211    interpolations into and inspect their values. This command loads the
   212    current state. This lets you explore and test interpolations before
   213    using them in future configurations.
   214  
   215    This command will never modify your state.
   216  
   217  Options:
   218  
   219    -state=path       Legacy option for the local backend only. See the local
   220                      backend's documentation for more information.
   221  
   222    -var 'foo=bar'    Set a variable in the OpenTofu configuration. This
   223                      flag can be set multiple times.
   224  
   225    -var-file=foo     Set variables in the OpenTofu configuration from
   226                      a file. If "terraform.tfvars" or any ".auto.tfvars"
   227                      files are present, they will be automatically loaded.
   228  `
   229  	return strings.TrimSpace(helpText)
   230  }
   231  
   232  func (c *ConsoleCommand) Synopsis() string {
   233  	return "Try OpenTofu expressions at an interactive command prompt"
   234  }