github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/command/console.go (about)

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