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