github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/command/console.go (about)

     1  package command
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/iaas-resource-provision/iaas-rpc/internal/addrs"
     9  	"github.com/iaas-resource-provision/iaas-rpc/internal/backend"
    10  	"github.com/iaas-resource-provision/iaas-rpc/internal/helper/wrappedstreams"
    11  	"github.com/iaas-resource-provision/iaas-rpc/internal/repl"
    12  	"github.com/iaas-resource-provision/iaas-rpc/internal/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 = c.Meta.process(args)
    25  	cmdFlags := c.Meta.extendedFlagSet("console")
    26  	cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
    27  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    28  	if err := cmdFlags.Parse(args); err != nil {
    29  		c.Ui.Error(fmt.Sprintf("Error parsing command line flags: %s\n", err.Error()))
    30  		return 1
    31  	}
    32  
    33  	configPath, err := ModulePath(cmdFlags.Args())
    34  	if err != nil {
    35  		c.Ui.Error(err.Error())
    36  		return 1
    37  	}
    38  	configPath = c.Meta.normalizePath(configPath)
    39  
    40  	// Check for user-supplied plugin path
    41  	if c.pluginPath, err = c.loadPluginPath(); err != nil {
    42  		c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err))
    43  		return 1
    44  	}
    45  
    46  	var diags tfdiags.Diagnostics
    47  
    48  	backendConfig, backendDiags := c.loadBackendConfig(configPath)
    49  	diags = diags.Append(backendDiags)
    50  	if diags.HasErrors() {
    51  		c.showDiagnostics(diags)
    52  		return 1
    53  	}
    54  
    55  	// Load the backend
    56  	b, backendDiags := c.Backend(&BackendOpts{
    57  		Config: backendConfig,
    58  	})
    59  	diags = diags.Append(backendDiags)
    60  	if backendDiags.HasErrors() {
    61  		c.showDiagnostics(diags)
    62  		return 1
    63  	}
    64  
    65  	// We require a local backend
    66  	local, ok := b.(backend.Local)
    67  	if !ok {
    68  		c.showDiagnostics(diags) // in case of any warnings in here
    69  		c.Ui.Error(ErrUnsupportedLocalOp)
    70  		return 1
    71  	}
    72  
    73  	// This is a read-only command
    74  	c.ignoreRemoteBackendVersionConflict(b)
    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  	diags = diags.Append(ctxDiags)
   100  	if ctxDiags.HasErrors() {
   101  		c.showDiagnostics(diags)
   102  		return 1
   103  	}
   104  
   105  	// Successfully creating the context can result in a lock, so ensure we release it
   106  	defer func() {
   107  		diags := opReq.StateLocker.Unlock()
   108  		if diags.HasErrors() {
   109  			c.showDiagnostics(diags)
   110  		}
   111  	}()
   112  
   113  	// Set up 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 [global options] console [options]
   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  Options:
   191  
   192    -state=path       Legacy option for the local backend only. See the local
   193                      backend's documentation for more information.
   194  
   195    -var 'foo=bar'    Set a variable in the Terraform configuration. This
   196                      flag can be set multiple times.
   197  
   198    -var-file=foo     Set variables in the Terraform configuration from
   199                      a file. If "terraform.tfvars" or any ".auto.tfvars"
   200                      files are present, they will be automatically loaded.
   201  `
   202  	return strings.TrimSpace(helpText)
   203  }
   204  
   205  func (c *ConsoleCommand) Synopsis() string {
   206  	return "Try IaaS-RPC expressions at an interactive command prompt"
   207  }