| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package main |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "log" |
| "net" |
| "os" |
| "path/filepath" |
| "runtime" |
| "strings" |
| |
| "github.com/hashicorp/go-plugin" |
| "github.com/hashicorp/terraform-svchost/disco" |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/command/cliconfig" |
| "github.com/hashicorp/terraform/internal/command/format" |
| "github.com/hashicorp/terraform/internal/didyoumean" |
| "github.com/hashicorp/terraform/internal/httpclient" |
| "github.com/hashicorp/terraform/internal/logging" |
| "github.com/hashicorp/terraform/internal/terminal" |
| "github.com/hashicorp/terraform/version" |
| "github.com/mattn/go-shellwords" |
| "github.com/mitchellh/cli" |
| "github.com/mitchellh/colorstring" |
| |
| backendInit "github.com/hashicorp/terraform/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 |
| |
| 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] Terraform 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 Terraform 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, 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. |
| } |
| } |
| providerDevOverrides := providerDevOverrides(config.ProviderInstallation) |
| |
| // The user can declare that certain providers are being managed on |
| // Terraform'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 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 != "" { |
| 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(originalWd, streams, config, services, providerSrc, providerDevOverrides, 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", |
| } |
| |
| // 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 |
| // "terraform state posh", 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, "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) |
| 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 |
| } |