github.com/pulumi/terraform@v1.4.0/pkg/command/console.go (about)

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