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