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 }