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