forked from hashicorp/terraform
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
513 lines (453 loc) · 14.9 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
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform-svchost/disco"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/command/cliconfig"
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/helper/logging"
"github.com/hashicorp/terraform/httpclient"
"github.com/hashicorp/terraform/version"
"github.com/mattn/go-colorable"
"github.com/mattn/go-shellwords"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
"github.com/mitchellh/panicwrap"
"github.com/mitchellh/prefixedio"
backendInit "github.com/hashicorp/terraform/backend/init"
)
const (
// EnvCLI is the environment variable name to set additional CLI args.
EnvCLI = "TF_CLI_ARGS"
)
func main() {
// Override global prefix set by go-dynect during init()
log.SetPrefix("")
os.Exit(realMain())
}
func realMain() int {
var wrapConfig panicwrap.WrapConfig
// don't re-exec terraform as a child process for easier debugging
if os.Getenv("TF_FORK") == "0" {
return wrappedMain()
}
if !panicwrap.Wrapped(&wrapConfig) {
// Determine where logs should go in general (requested by the user)
logWriter, err := logging.LogOutput()
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err)
return 1
}
// We always send logs to a temporary file that we use in case
// there is a panic. Otherwise, we delete it.
logTempFile, err := ioutil.TempFile("", "terraform-log")
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err)
return 1
}
defer os.Remove(logTempFile.Name())
defer logTempFile.Close()
// Setup the prefixed readers that send data properly to
// stdout/stderr.
doneCh := make(chan struct{})
outR, outW := io.Pipe()
go copyOutput(outR, doneCh)
// Create the configuration for panicwrap and wrap our executable
wrapConfig.Handler = panicHandler(logTempFile)
wrapConfig.Writer = io.MultiWriter(logTempFile, logWriter)
wrapConfig.Stdout = outW
wrapConfig.IgnoreSignals = ignoreSignals
wrapConfig.ForwardSignals = forwardSignals
exitStatus, err := panicwrap.Wrap(&wrapConfig)
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't start Terraform: %s", err)
return 1
}
// If >= 0, we're the parent, so just exit
if exitStatus >= 0 {
// Close the stdout writer so that our copy process can finish
outW.Close()
// Wait for the output copying to finish
<-doneCh
return exitStatus
}
// We're the child, so just close the tempfile we made in order to
// save file handles since the tempfile is only used by the parent.
logTempFile.Close()
}
// Call the real main
return wrappedMain()
}
func init() {
Ui = &cli.PrefixedUi{
AskPrefix: OutputPrefix,
OutputPrefix: OutputPrefix,
InfoPrefix: OutputPrefix,
ErrorPrefix: ErrorPrefix,
Ui: &cli.BasicUi{
Writer: os.Stdout,
Reader: os.Stdin,
},
}
}
func wrappedMain() int {
var err error
log.SetOutput(os.Stderr)
log.Printf(
"[INFO] Terraform version: %s %s %s",
Version, VersionPrerelease, GitCommit)
log.Printf("[INFO] Go runtime version: %s", runtime.Version())
log.Printf("[INFO] CLI args: %#v", os.Args)
// 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, Terraform may not behave as intended.\n\n")
// We continue to run anyway, since Terraform 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.TerraformUserAgent(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, Terraform's provider installer may not behave as intended.\n\n")
// We continue to run anyway, because most commands don't do provider installation.
}
}
// The user can declare that certain providers are being managed on
// Terraform's behalf using this environment variable. Thsi 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 Terraform 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 != "" {
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(originalWd, config, services, providerSrc, unmanagedProviders)
}
// Run checkpoint
go runCheckpoint(config)
// 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",
}
// Pass in the overriding plugin paths from config
PluginOverrides.Providers = config.Providers
PluginOverrides.Provisioners = config.Provisioners
exitCode, err := cliRunner.Run()
if err != nil {
Ui.Error(fmt.Sprintf("Error executing CLI: %s", err.Error()))
return 1
}
return exitCode
}
// copyOutput uses output prefixes to determine whether data on stdout
// should go to stdout or stderr. This is due to panicwrap using stderr
// as the log and error channel.
func copyOutput(r io.Reader, doneCh chan<- struct{}) {
defer close(doneCh)
pr, err := prefixedio.NewReader(r)
if err != nil {
panic(err)
}
stderrR, err := pr.Prefix(ErrorPrefix)
if err != nil {
panic(err)
}
stdoutR, err := pr.Prefix(OutputPrefix)
if err != nil {
panic(err)
}
defaultR, err := pr.Prefix("")
if err != nil {
panic(err)
}
var stdout io.Writer = os.Stdout
var stderr io.Writer = os.Stderr
if runtime.GOOS == "windows" {
stdout = colorable.NewColorableStdout()
stderr = colorable.NewColorableStderr()
// colorable is not concurrency-safe when stdout and stderr are the
// same console, so we need to add some synchronization to ensure that
// we can't be concurrently writing to both stderr and stdout at
// once, or else we get intermingled writes that create gibberish
// in the console.
wrapped := synchronizedWriters(stdout, stderr)
stdout = wrapped[0]
stderr = wrapped[1]
}
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
io.Copy(stderr, stderrR)
}()
go func() {
defer wg.Done()
io.Copy(stdout, stdoutR)
}()
go func() {
defer wg.Done()
io.Copy(stdout, defaultR)
}()
wg.Wait()
}
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
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),
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
}