github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/command/console.go (about)

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