mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-24 08:00:17 -06:00
537 lines
17 KiB
Go
537 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"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/httpclient"
|
|
"github.com/hashicorp/terraform/internal/didyoumean"
|
|
"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"
|
|
"github.com/mitchellh/panicwrap"
|
|
|
|
backendInit "github.com/hashicorp/terraform/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"
|
|
|
|
// Environment variable name used for smuggling true stderr terminal
|
|
// settings into a panicwrap child process. This is an implementation
|
|
// detail, subject to change in future, and should not ever be directly
|
|
// set by an end-user.
|
|
envTerminalPanicwrapWorkaround = "TF_PANICWRAP_STDERR"
|
|
)
|
|
|
|
// 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 main() {
|
|
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) {
|
|
// 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 set up logging tempfile: %s", err)
|
|
return 1
|
|
}
|
|
// Now that we have the file, close it and leave it for the wrapped
|
|
// process to write to.
|
|
logTempFile.Close()
|
|
defer os.Remove(logTempFile.Name())
|
|
|
|
// store the path in the environment for the wrapped executable
|
|
os.Setenv(envTmpLogPath, logTempFile.Name())
|
|
|
|
// We also need to do our terminal initialization before we fork,
|
|
// because the child process doesn't necessarily have access to
|
|
// the true stderr in order to initialize it.
|
|
streams, err := terminal.Init()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to initialize terminal: %s", err)
|
|
return 1
|
|
}
|
|
|
|
// We need the child process to behave _as if_ connected to the real
|
|
// stderr, even though panicwrap is about to add a pipe in the way,
|
|
// so we'll smuggle the true stderr information in an environment
|
|
// varible.
|
|
streamState := streams.StateForAfterPanicWrap()
|
|
os.Setenv(envTerminalPanicwrapWorkaround, fmt.Sprintf("%t:%d", streamState.StderrIsTerminal, streamState.StderrWidth))
|
|
|
|
// Create the configuration for panicwrap and wrap our executable
|
|
wrapConfig.Handler = logging.PanicHandler(logTempFile.Name())
|
|
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
|
|
}
|
|
|
|
return exitStatus
|
|
}
|
|
|
|
// Call the real main
|
|
return wrappedMain()
|
|
}
|
|
|
|
func init() {
|
|
Ui = &ui{&cli.BasicUi{
|
|
Writer: os.Stdout,
|
|
ErrorWriter: os.Stderr,
|
|
Reader: os.Stdin,
|
|
}}
|
|
}
|
|
|
|
func wrappedMain() int {
|
|
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)
|
|
log.Printf("[INFO] Go runtime version: %s", runtime.Version())
|
|
log.Printf("[INFO] CLI args: %#v", os.Args)
|
|
|
|
// This is the recieving end of our workaround to retain the metadata
|
|
// about the real stderr even though we're talking to it via the panicwrap
|
|
// pipe. See the call to StateForAfterPanicWrap above for the producer
|
|
// part of this.
|
|
var streamState *terminal.PrePanicwrapState
|
|
if raw := os.Getenv(envTerminalPanicwrapWorkaround); raw != "" {
|
|
streamState = &terminal.PrePanicwrapState{}
|
|
if _, err := fmt.Sscanf(raw, "%t:%d", &streamState.StderrIsTerminal, &streamState.StderrWidth); err != nil {
|
|
log.Printf("[WARN] %s is set but is incorrectly-formatted: %s", envTerminalPanicwrapWorkaround, err)
|
|
streamState = nil // leave it unset for a normal init, then
|
|
}
|
|
}
|
|
streams, err := terminal.ReinitInsidePanicwrap(streamState)
|
|
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. 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 != "" {
|
|
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.)
|
|
if cmd := cliRunner.Subcommand(); cmd != "" {
|
|
// 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() {
|
|
// we don't write this to Error, or else panicwrap will think this
|
|
// process panicked
|
|
Ui.Info(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
|
|
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
|
|
}
|