forked from opentofu/opentofu
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
493 lines (441 loc) · 15.8 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/apparentlymart/go-shquot/shquot"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform-svchost/disco"
"github.com/mattn/go-shellwords"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
"github.com/placeholderplaceholderplaceholder/opentf/internal/addrs"
"github.com/placeholderplaceholderplaceholder/opentf/internal/command/cliconfig"
"github.com/placeholderplaceholderplaceholder/opentf/internal/command/format"
"github.com/placeholderplaceholderplaceholder/opentf/internal/didyoumean"
"github.com/placeholderplaceholderplaceholder/opentf/internal/httpclient"
"github.com/placeholderplaceholderplaceholder/opentf/internal/logging"
"github.com/placeholderplaceholderplaceholder/opentf/internal/terminal"
"github.com/placeholderplaceholderplaceholder/opentf/version"
"go.opentelemetry.io/otel/trace"
backendInit "github.com/placeholderplaceholderplaceholder/opentf/internal/backend/init"
)
const (
// EnvCLI is the environment variable name to set additional CLI args.
EnvCLI = "TF_CLI_ARGS"
// The parent process will create a file to collect crash logs
envTmpLogPath = "TF_TEMP_LOG_PATH"
)
// ui wraps the primary output cli.Ui, and redirects Warn calls to Output
// calls. This ensures that warnings are sent to stdout, and are properly
// serialized within the stdout stream.
type ui struct {
cli.Ui
}
func (u *ui) Warn(msg string) {
u.Ui.Output(msg)
}
func init() {
Ui = &ui{&cli.BasicUi{
Writer: os.Stdout,
ErrorWriter: os.Stderr,
Reader: os.Stdin,
}}
}
func main() {
os.Exit(realMain())
}
func realMain() int {
defer logging.PanicHandler()
var err error
err = openTelemetryInit()
if err != nil {
// openTelemetryInit can only fail if OpenTF was run with an
// explicit environment variable to enable telemetry collection,
// so in typical use we cannot get here.
Ui.Error(fmt.Sprintf("Could not initialize telemetry: %s", err))
Ui.Error(fmt.Sprintf("Unset environment variable %s if you don't intend to collect telemetry from OpenTF.", openTelemetryExporterEnvVar))
return 1
}
var ctx context.Context
var otelSpan trace.Span
{
// At minimum we emit a span covering the entire command execution.
_, displayArgs := shquot.POSIXShellSplit(os.Args)
ctx, otelSpan = tracer.Start(context.Background(), fmt.Sprintf("opentf %s", displayArgs))
defer otelSpan.End()
}
tmpLogPath := os.Getenv(envTmpLogPath)
if tmpLogPath != "" {
f, err := os.OpenFile(tmpLogPath, os.O_RDWR|os.O_APPEND, 0666)
if err == nil {
defer f.Close()
log.Printf("[DEBUG] Adding temp file log sink: %s", f.Name())
logging.RegisterSink(f)
} else {
log.Printf("[ERROR] Could not open temp log file: %v", err)
}
}
log.Printf(
"[INFO] OpenTF version: %s %s",
Version, VersionPrerelease)
for _, depMod := range version.InterestingDependencies() {
log.Printf("[DEBUG] using %s %s", depMod.Path, depMod.Version)
}
log.Printf("[INFO] Go runtime version: %s", runtime.Version())
log.Printf("[INFO] CLI args: %#v", os.Args)
if ExperimentsAllowed() {
log.Printf("[INFO] This build of OpenTF allows using experimental features")
}
streams, err := terminal.Init()
if err != nil {
Ui.Error(fmt.Sprintf("Failed to configure the terminal: %s", err))
return 1
}
if streams.Stdout.IsTerminal() {
log.Printf("[TRACE] Stdout is a terminal of width %d", streams.Stdout.Columns())
} else {
log.Printf("[TRACE] Stdout is not a terminal")
}
if streams.Stderr.IsTerminal() {
log.Printf("[TRACE] Stderr is a terminal of width %d", streams.Stderr.Columns())
} else {
log.Printf("[TRACE] Stderr is not a terminal")
}
if streams.Stdin.IsTerminal() {
log.Printf("[TRACE] Stdin is a terminal")
} else {
log.Printf("[TRACE] Stdin is not a terminal")
}
// NOTE: We're intentionally calling LoadConfig _before_ handling a possible
// -chdir=... option on the command line, so that a possible relative
// path in the TERRAFORM_CONFIG_FILE environment variable (though probably
// ill-advised) will be resolved relative to the true working directory,
// not the overridden one.
config, diags := cliconfig.LoadConfig()
if len(diags) > 0 {
// Since we haven't instantiated a command.Meta yet, we need to do
// some things manually here and use some "safe" defaults for things
// that command.Meta could otherwise figure out in smarter ways.
Ui.Error("There are some problems with the CLI configuration:")
for _, diag := range diags {
earlyColor := &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: true, // Disable color to be conservative until we know better
Reset: true,
}
// We don't currently have access to the source code cache for
// the parser used to load the CLI config, so we can't show
// source code snippets in early diagnostics.
Ui.Error(format.Diagnostic(diag, nil, earlyColor, 78))
}
if diags.HasErrors() {
Ui.Error("As a result of the above problems, OpenTF may not behave as intended.\n\n")
// We continue to run anyway, since OpenTF has reasonable defaults.
}
}
// Get any configured credentials from the config and initialize
// a service discovery object. The slightly awkward predeclaration of
// disco is required to allow us to pass untyped nil as the creds source
// when creating the source fails. Otherwise we pass a typed nil which
// breaks the nil checks in the disco object
var services *disco.Disco
credsSrc, err := credentialsSource(config)
if err == nil {
services = disco.NewWithCredentialsSource(credsSrc)
} else {
// Most commands don't actually need credentials, and most situations
// that would get us here would already have been reported by the config
// loading above, so we'll just log this one as an aid to debugging
// in the unlikely event that it _does_ arise.
log.Printf("[WARN] Cannot initialize remote host credentials manager: %s", err)
// passing (untyped) nil as the creds source is okay because the disco
// object checks that and just acts as though no credentials are present.
services = disco.NewWithCredentialsSource(nil)
}
services.SetUserAgent(httpclient.OpenTfUserAgent(version.String()))
providerSrc, diags := providerSource(config.ProviderInstallation, services)
if len(diags) > 0 {
Ui.Error("There are some problems with the provider_installation configuration:")
for _, diag := range diags {
earlyColor := &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: true, // Disable color to be conservative until we know better
Reset: true,
}
Ui.Error(format.Diagnostic(diag, nil, earlyColor, 78))
}
if diags.HasErrors() {
Ui.Error("As a result of the above problems, OpenTF's provider installer may not behave as intended.\n\n")
// We continue to run anyway, because most commands don't do provider installation.
}
}
providerDevOverrides := providerDevOverrides(config.ProviderInstallation)
// The user can declare that certain providers are being managed on
// OpenTF's behalf using this environment variable. This is used
// primarily by the SDK's acceptance testing framework.
unmanagedProviders, err := parseReattachProviders(os.Getenv("TF_REATTACH_PROVIDERS"))
if err != nil {
Ui.Error(err.Error())
return 1
}
// Initialize the backends.
backendInit.Init(services)
// Get the command line args.
binName := filepath.Base(os.Args[0])
args := os.Args[1:]
originalWd, err := os.Getwd()
if err != nil {
// It would be very strange to end up here
Ui.Error(fmt.Sprintf("Failed to determine current working directory: %s", err))
return 1
}
// The arguments can begin with a -chdir option to ask OpenTF to switch
// to a different working directory for the rest of its work. If that
// option is present then extractChdirOption returns a trimmed args with that option removed.
overrideWd, args, err := extractChdirOption(args)
if err != nil {
Ui.Error(fmt.Sprintf("Invalid -chdir option: %s", err))
return 1
}
if overrideWd != "" {
err := os.Chdir(overrideWd)
if err != nil {
Ui.Error(fmt.Sprintf("Error handling -chdir option: %s", err))
return 1
}
}
// In tests, Commands may already be set to provide mock commands
if Commands == nil {
// Commands get to hold on to the original working directory here,
// in case they need to refer back to it for any special reason, though
// they should primarily be working with the override working directory
// that we've now switched to above.
initCommands(ctx, originalWd, streams, config, services, providerSrc, providerDevOverrides, unmanagedProviders)
}
// Make sure we clean up any managed plugins at the end of this
defer plugin.CleanupClients()
// Build the CLI so far, we do this so we can query the subcommand.
cliRunner := &cli.CLI{
Args: args,
Commands: Commands,
HelpFunc: helpFunc,
HelpWriter: os.Stdout,
}
// Prefix the args with any args from the EnvCLI
args, err = mergeEnvArgs(EnvCLI, cliRunner.Subcommand(), args)
if err != nil {
Ui.Error(err.Error())
return 1
}
// Prefix the args with any args from the EnvCLI targeting this command
suffix := strings.Replace(strings.Replace(
cliRunner.Subcommand(), "-", "_", -1), " ", "_", -1)
args, err = mergeEnvArgs(
fmt.Sprintf("%s_%s", EnvCLI, suffix), cliRunner.Subcommand(), args)
if err != nil {
Ui.Error(err.Error())
return 1
}
// We shortcut "--version" and "-v" to just show the version
for _, arg := range args {
if arg == "-v" || arg == "-version" || arg == "--version" {
newArgs := make([]string, len(args)+1)
newArgs[0] = "version"
copy(newArgs[1:], args)
args = newArgs
break
}
}
// Rebuild the CLI with any modified args.
log.Printf("[INFO] CLI command args: %#v", args)
cliRunner = &cli.CLI{
Name: binName,
Args: args,
Commands: Commands,
HelpFunc: helpFunc,
HelpWriter: os.Stdout,
Autocomplete: true,
AutocompleteInstall: "install-autocomplete",
AutocompleteUninstall: "uninstall-autocomplete",
}
// Before we continue we'll check whether the requested command is
// actually known. If not, we might be able to suggest an alternative
// if it seems like the user made a typo.
// (This bypasses the built-in help handling in cli.CLI for the situation
// where a command isn't found, because it's likely more helpful to
// mention what specifically went wrong, rather than just printing out
// a big block of usage information.)
// Check if this is being run via shell auto-complete, which uses the
// binary name as the first argument and won't be listed as a subcommand.
autoComplete := os.Getenv("COMP_LINE") != ""
if cmd := cliRunner.Subcommand(); cmd != "" && !autoComplete {
// Due to the design of cli.CLI, this special error message only works
// for typos of top-level commands. For a subcommand typo, like
// "opentf state push", cmd would be "state" here and thus would
// be considered to exist, and it would print out its own usage message.
if _, exists := Commands[cmd]; !exists {
suggestions := make([]string, 0, len(Commands))
for name := range Commands {
suggestions = append(suggestions, name)
}
suggestion := didyoumean.NameSuggestion(cmd, suggestions)
if suggestion != "" {
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
}
fmt.Fprintf(os.Stderr, "OpenTF has no command named %q.%s\n\nTo see all of OpenTF's top-level commands, run:\n opentf -help\n\n", cmd, suggestion)
return 1
}
}
exitCode, err := cliRunner.Run()
if err != nil {
Ui.Error(fmt.Sprintf("Error executing CLI: %s", err.Error()))
return 1
}
// if we are exiting with a non-zero code, check if it was caused by any
// plugins crashing
if exitCode != 0 {
for _, panicLog := range logging.PluginPanics() {
Ui.Error(panicLog)
}
}
return exitCode
}
func mergeEnvArgs(envName string, cmd string, args []string) ([]string, error) {
v := os.Getenv(envName)
if v == "" {
return args, nil
}
log.Printf("[INFO] %s value: %q", envName, v)
extra, err := shellwords.Parse(v)
if err != nil {
return nil, fmt.Errorf(
"Error parsing extra CLI args from %s: %s",
envName, err)
}
// Find the command to look for in the args. If there is a space,
// we need to find the last part.
search := cmd
if idx := strings.LastIndex(search, " "); idx >= 0 {
search = cmd[idx+1:]
}
// Find the index to place the flags. We put them exactly
// after the first non-flag arg.
idx := -1
for i, v := range args {
if v == search {
idx = i
break
}
}
// idx points to the exact arg that isn't a flag. We increment
// by one so that all the copying below expects idx to be the
// insertion point.
idx++
// Copy the args
newArgs := make([]string, len(args)+len(extra))
copy(newArgs, args[:idx])
copy(newArgs[idx:], extra)
copy(newArgs[len(extra)+idx:], args[idx:])
return newArgs, nil
}
// parse information on reattaching to unmanaged providers out of a
// JSON-encoded environment variable.
func parseReattachProviders(in string) (map[addrs.Provider]*plugin.ReattachConfig, error) {
unmanagedProviders := map[addrs.Provider]*plugin.ReattachConfig{}
if in != "" {
type reattachConfig struct {
Protocol string
ProtocolVersion int
Addr struct {
Network string
String string
}
Pid int
Test bool
}
var m map[string]reattachConfig
err := json.Unmarshal([]byte(in), &m)
if err != nil {
return unmanagedProviders, fmt.Errorf("Invalid format for TF_REATTACH_PROVIDERS: %w", err)
}
for p, c := range m {
a, diags := addrs.ParseProviderSourceString(p)
if diags.HasErrors() {
return unmanagedProviders, fmt.Errorf("Error parsing %q as a provider address: %w", a, diags.Err())
}
var addr net.Addr
switch c.Addr.Network {
case "unix":
addr, err = net.ResolveUnixAddr("unix", c.Addr.String)
if err != nil {
return unmanagedProviders, fmt.Errorf("Invalid unix socket path %q for %q: %w", c.Addr.String, p, err)
}
case "tcp":
addr, err = net.ResolveTCPAddr("tcp", c.Addr.String)
if err != nil {
return unmanagedProviders, fmt.Errorf("Invalid TCP address %q for %q: %w", c.Addr.String, p, err)
}
default:
return unmanagedProviders, fmt.Errorf("Unknown address type %q for %q", c.Addr.Network, p)
}
unmanagedProviders[a] = &plugin.ReattachConfig{
Protocol: plugin.Protocol(c.Protocol),
ProtocolVersion: c.ProtocolVersion,
Pid: c.Pid,
Test: c.Test,
Addr: addr,
}
}
}
return unmanagedProviders, nil
}
func extractChdirOption(args []string) (string, []string, error) {
if len(args) == 0 {
return "", args, nil
}
const argName = "-chdir"
const argPrefix = argName + "="
var argValue string
var argPos int
for i, arg := range args {
if !strings.HasPrefix(arg, "-") {
// Because the chdir option is a subcommand-agnostic one, we require
// it to appear before any subcommand argument, so if we find a
// non-option before we find -chdir then we are finished.
break
}
if arg == argName || arg == argPrefix {
return "", args, fmt.Errorf("must include an equals sign followed by a directory path, like -chdir=example")
}
if strings.HasPrefix(arg, argPrefix) {
argPos = i
argValue = arg[len(argPrefix):]
}
}
// When we fall out here, we'll have populated argValue with a non-empty
// string if the -chdir=... option was present and valid, or left it
// empty if it wasn't present.
if argValue == "" {
return "", args, nil
}
// If we did find the option then we'll need to produce a new args that
// doesn't include it anymore.
if argPos == 0 {
// Easy case: we can just slice off the front
return argValue, args[1:], nil
}
// Otherwise we need to construct a new array and copy to it.
newArgs := make([]string, len(args)-1)
copy(newArgs, args[:argPos])
copy(newArgs[argPos:], args[argPos+1:])
return argValue, newArgs, nil
}