github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/console.go (about)

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